當前位置:
首頁 > 最新 > 被遺棄的線程

被遺棄的線程

main函數作為程序運行的入口,正常情況下,函數會執行毫秒級別的操作,然後返回一個0表示程序正常終止。為了避免應用啟動即終止,蘋果設計了runloop機制來維持線程的生命,runloop在每一次的循環當中不斷的去處理事件,或控制線程的休眠和喚醒。runloop還結合了libdispatch的任務派發機制,可以循環地處理async到隊列中的任務

啟動runloop

從runloop對外暴露的介面來看,啟動方式一共存在三種:

無條件的啟動。這種啟動方式缺少停止runloop的手段,唯一的結束方式是kill掉線程

設定超時時間。直接run會調用這個介面並且傳入distantFuture,表示無限長的超時時間。如果超時時間不是無限長,那麼runloop會在處理完事件或者超時後終止,優於直接run

設置超時時間和運行模式。比起第二種,允許我們讓runloop運行在某個模式下,靈活性更高

如果runloop在啟動之後沒有任何sources、timers或者ports事件可以處理,那麼會自動退出,否則會在處理完成後讓線程陷入休眠,等待這些事件重新喚醒線程處理。下面是最常用來表示runloop處理邏輯的示意圖:

除開圖中列出的事件之外,main loop會處理timer之後檢測隊列中是否存在待執行的block然後開始執行

什麼情況下需要啟動runloop

主線程的runloop會在應用啟動後被UIApplication啟動,其他線程則需要我們主動去run。從接觸iOS開發到現在,筆者了解的需要主動啟動runloop只有這麼兩類:

子線程使用timer

由於NSTimer本身就不是一個能確保穩定回調的定時器機制,並且主線程會經常處在忙碌狀態,這又進一步降低了NSTimer的準確性。為了提高定時的準確性,多數人會採用子線程啟動runloop的方式來實現定時器功能

線程保活

線程保活是一種不太常見的需求,但是如果你曾經了解過AFNetworking的做法,會發現創建了子線程之後,採用一個空port的方式來啟動runloop,避免線程被中止回收。但實際上這種做法很容易導致線程既無法被回收,也不能被使用的情況

上面兩種不同的應用場景,實際上是使用timer和port維持runloop不會因為沒有事件處理直接退出,而且在這些源事件來臨之前,線程大多數情況下處在休眠狀態不造成額外損耗

串列隊列的runloop

假設現在需要使用一個子線程的runloop來實現定時器,由於runloop在停止之前,線程會一直存活,因此可能會想利用這個存活的線程處理其他的任務。因此除了NSTimer之外,我們添加一個GCD Timer定時的派發任務給這個啟動runloop的隊列:

按照預期,這段代碼充分利用了已經被保活的線程,除了已有的NSTimer之外,線程還能在空閑的時間去處理不斷派發的任務,但實際上只有NSTimer的任務被執行:

導致保活線程無法處理async任務的原因有兩個:

runloop和queue的區別

runloop和queue各自維護著自己的一個任務隊列,在runloop的每個周期裡面,會檢測自身的任務隊列裡面是否存在待執行的task並且執行。但主線程的情況比較特殊,在main runloop的每個周期,會去檢測main queue是否存在待執行任務,如果存在,那麼copy到自身的任務隊列中執行

async的實現不同

在非主線程之外,runloop和queue的任務隊列是互不干擾的,因此兩者處理任務的機制也是完全不同的。當async任務到隊列時,GCD會嘗試尋找一個線程來執行任務。由於串列隊列同時只能與一個線程掛鉤,因此GCD會讓該線程執行完已有任務後,才執行async到隊列中的任務。但由於線程被保活,任務是一個條件死循環condition-loop,因此async的任務始終無法被處理

為了證明這些原因,可以通過CFRunLoopPerformBlock將任務直接加入到runloop自身的任務隊列中,檢測這個任務是否被執行:

再次運行之後,async的任務依舊無法被處理,但是perform block的任務總是能在timer喚醒休眠的線程後被處理:

並行隊列的runloop

隊列的串並行屬性決定了隊列能不能被多個線程處理任務,因此同樣的代碼在並行隊列執行,產生的結果必然是有所區別的:

輸出結果如下:

雖然並行隊列的async功能並不會因為啟動了runloop受到影響,但是可以發現如果不去保存runloop,這個保活的線程除了定時器能正常處理之外,其他時候不會再被GCD復用

使用port保活

如果不使用NSTimer這種穩定的喚醒機制來保活線程,而是採用port的方式,線程的表現是否依舊符合預期?

此時任務總是不會被處理。由於runloop需要被喚醒才能處理隊列任務,而perform block只是單純的添加任務,沒有喚醒功能。為了線程能夠繼續執行任務,這時候還需要不斷的wake up線程:

結論

從測試來看,在子線程啟動runloop並不是一個很明智的選擇:這會導致線程保活期間被遺棄,失去了處理消息派發的能力,且無法響應其他線程的通信。其次,即便可以通過perform block來繼續為保活線程添加任務處理,但在保活線程的runloop缺乏穩定的喚醒機制的情況下,還需要其他線程來提供喚醒能力,這增加了代碼設計的成本,並且不會有額外的好處


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Cocoa開發者社區 的精彩文章:

iOS 性能優化探索
李笑來:區塊鏈技術終將改變世界,長期樂觀,短期保守

TAG:Cocoa開發者社區 |