iOS執行緒學習筆記

躍然發表於2017-05-31

文字源自對以下文章的摘抄:
1. threading-programming-guide筆記一
2. threading-programming-guide筆記二
3. threading-programming-guide筆記三
4. threading-programming-guide筆記四

感謝原作者。
這裡摘抄,只為學習目的,以便日後再複習。

一、OS X和iOS中提供的不那麼底層的實現多工併發執行的解決方案:

Operation object:該技術出現在OS X 10.5中,通過將要執行的任務封裝成操作物件的方式實現任務在多執行緒中執行。任務可以理解為你要想執行的一段程式碼。在這個操作物件中不光包含要執行的任務,還包含執行緒管理的內容,使用時通常與操作佇列物件聯合使用,操作佇列物件會管理操作物件如何使用執行緒,所以我們只需要關心要執行的任務本身即可。

  • GCD:該技術出現在OS X 10.6中,它與Operation Object的初衷類似,就是讓開發者只關注要執行的任務本身,而不需要去關注執行緒的管理。你只需要建立好任務,然後將任務新增到一個工作佇列裡即可,該工作佇列會根據當前CPU效能及核心的負載情況,將任務安排到合適的執行緒中去執行。

  • Idle-time notification:該技術主要用於處理優先順序相對比較低、執行時間比較短的任務,讓應用程式在空閒的時候執行這類任務。Cocoa框架提供NSNotificationQueue物件處理空閒時間通知,通過使用NSPostWhenIdle選項,向佇列傳送空閒時間通知的請求。

  • Asynchronous functions:系統中有一些支援非同步的函式,可以自動讓你的程式碼並行執行。這些非同步函式可能通過應用程式的守護程式或者自定義的執行緒執行你的程式碼,與主程式或主執行緒分離,達到並行執行任務的功能。

  • Timers:我們也可以在應用程式主執行緒中使用定時器去執行一些比較輕量級的、有一定週期性的任務。

  • Separate processes:雖然通過另起一個程式比執行緒更加重量級,但是在某些情況下要比使用執行緒更好一些,比如你需要的執行的任務和你的應用程式在展現資料和使用方面沒有什麼關係,但是可以優化你的應用程式的執行環境,或者提高應用程式獲取資料的效率等。

在應用程式層面,不管是什麼平臺,執行緒的執行方式都是大體相同的,線上程的執行過程中一般都會經歷三種狀態,即執行中、準備執行、阻塞。

二、RunLoop

參考:threading-programming-guide筆記三

簡單的來說,RunLoop用於管理和監聽非同步新增到執行緒中的事件,當有事件輸入時,系統喚醒執行緒並將事件分派給RunLoop,當沒有需要處理的事件時,RunLoop會讓執行緒進入休眠狀態。這樣就能讓執行緒常駐在程式中,而不會過多的消耗系統資源,達到有事做事,沒事睡覺的效果。

Run Loop線上程中的主要作用就是幫助執行緒常駐在程式中,並且不會過多消耗資源。所以說Run Loop在二級執行緒中也不是必須需要的,要根據該執行緒執行的任務型別以及在整個應用中擔任何作用而決定是否需要使用Run Loop。比如說,如果你建立一個二級執行緒只是為了執行一個不會頻繁執行的一次性任務,或者需要執行很長時間的任務,那麼可能就不需要使用Run Loop了。如果你需要一個執行緒執行週期性的定時任務,或者需要較為頻繁的與主執行緒之間進行互動,那麼就需要使用Run Loop。

使用Run Loop的情況大概有以下四點:

  • 通過基於埠或自定義的資料來源與其他執行緒進行互動。
  • 線上程中執行定時事件源的任務。
  • 使用Cocoa框架提供的performSelector…系列方法。
  • 線上程中執行較為頻繁的,具有周期性的任務。
Run Loop物件

要想操作配置Run Loop,那自然需要通過Run Loop物件來完成,它提供了一系列介面,可幫助我們便捷的新增Input sources、timers以及觀察者。較高階別的Cocoa框架提供了NSRunLoop類,較底層級別的Core Foundation框架提供了指向CFRunloopRef的指標。

獲取Run Loop物件

在Cocoa和Core Foundation框架中都沒有提供建立Run Loop的方法,只有從當前執行緒獲取Run Loop的方法:

  • 在Cocoa框架中,NSRunLoop類提供了類方法currentRunLoop()獲取NSRunLoop物件。
    該方法是獲取當前執行緒中已存在的Run Loop,如果不存在,那其實還是會建立一個Run Loop物件返回,只是Cocoa框架沒有向我們暴露該介面。
  • 在Core Foundation框架中提供了CFRunLoopGetCurrent()函式獲取CFRunLoop物件

雖然這兩個Run Loop物件並不完全等價,它們之間還是可以轉換的,我們可以通過NSRunLoop物件提供的getCFRunLoop()方法獲取CFRunLoop物件。因為NSRunLoop和CFRunLoop指向的都是當前執行緒中同一個Run Loop,所以在使用時它們可以混用,比如說要給Run Loop新增觀察者時就必須得用CFRunLoop了。

配置Run Loop觀察者

可以向Run Loop中新增各種事件源和觀察者,這裡事件源是必填項,也就是說Run Loop中至少要有一種事件源,不論是Input source還是timer,如果Run Loop中沒有事件源的話,那麼在啟動Run Loop後就會立即退出。而觀察者是可選項,如果沒有監控Run Loop各執行狀態的需求,可以不配置觀察者。

啟動Run Loop

在啟動Run Loop前務必要保證已新增一種型別的事件源。在Cocoa框架和Core Foundation框架中啟動Run Loop大體有三種形式,分別是無條件啟動、設定時間限制啟動、指定特定模式啟動。

1.無條件啟動

NSRunLoop物件的run()方法和Core Foundation框架中的CFRunLoopRun()函式都是無條件啟動Run Loop的方式。這種方式雖然是最簡單的啟動方式,但也是最不推薦使用的一個方式,因為這種方式將Run Loop置於一個永久執行並且不可控的狀態,它使Run Loop只能在預設模式下執行,無法給Run Loop設定特定的或自定義的模式,而且以這種模式啟動的Run Loop只能通過CFRunLoopStop(_ rl: CFRunLoop!)函式強制停止。

2.設定時間限制啟動

該方式對應的方法是NSRunLoop物件的runUntilDate(_ limitDate: NSDate)方法,在啟動Run Loop時設定超時時間,一旦超時那麼Run Loop則自動退出。該方法的好處是可以在迴圈中反覆啟動Run Loop處理相關任務,而且可控制執行時長。

3.指定特定模式啟動

該方式對應的方法是NSRunLoop物件的runMode(_ mode: String, beforeDate limitDate: NSDate)方法和Core Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)函式。前者有兩個引數,第一個引數是Run Loop模式,第二個引數仍然是超時時間,該方法使Run Loop只處理指定模式中的事件源事件,當處理完事件或超時Run Loop會退出,該方法的返回值型別是Bool,如果返回true則表示Run Loop啟動成功,並分派執行了任務或者達到超時時間,若返回false則表示Run Loop啟動失敗。後者有三個引數,前兩個引數的作用一樣,第三個引數的意思是Run Loop是否在執行完任務後就退出,如果設定為false,那麼代表Run Loop在執行完任務後不退出,而是一直等到超時後才退出。該方法返回Run Loop的退出狀態:

  • CFRunLoopRunResult.Finished:表示Run Loop已分派執行完任務,並且再無任務執行的情況下退出。
  • CFRunLoopRunResult.Stopped:表示Run Loop通過CFRunLoopStop(_ rl: CFRunLoop!)函式強制退出。
  • CFRunLoopRunResult.TimedOut:表示Run Loop因為超時時間到而退出。
  • CFRunLoopRunResult.HandledSource:表示Run Loop已執行完任務而退出,改狀態只有在returnAfterSourceHandled設定為true時才會出現。
退出Run Loop

退出Run Loop的方式總體來說有三種:

  • 啟動Run Loop時設定超時時間。
  • 強制退出Run Loop。
  • 移除Run Loop中的事件源,從而使Run Loop退出。

第一種方式是推薦使用的方式,因為可以給Run Loop設定可控的執行時間,讓它執行完所有的任務以及給觀察者傳送通知。第二種強制退出Run Loop主要是應對無條件啟動Run Loop的情況。第三種方式是最不推薦的方式,雖然在理論上說當Run Loop中沒有任何資料來源時會立即退出,但是在實際情況中我們建立的二級執行緒除了執行我們指定的任務外,有可能系統還會讓其執行一些系統層面的任務,而且這些任務我們一般無法知曉,所以用這種方式退出Run Loop往往會存在延遲退出。

Run Loop物件的執行緒安全性

Run Loop物件的執行緒安全性取決於我們使用哪種API去操作。Core Foundation框架中的CFRunLoop物件是執行緒安全的,我們可以在任何執行緒中使用。Cocoa框架的NSRunLoop物件是執行緒不安全的,我們必須在擁有Run Loop的當前執行緒中操作Run Loop,如果操作了不屬於當前執行緒的Run loop,會導致異常和各種潛在的問題發生。

自定義Run Loop事件源

Cocoa框架因為是較為高層的框架,所以沒有提供操作較為底層的Run Loop事件源相關的介面和物件,所以我們只能使用Core Foundation框架中的物件和函式建立事件源並給Run Loop設定事件源。

1.建立Run Loop事件源物件
建立事件源的方法:

func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
  1. allocator:該引數為物件記憶體分配器,一般使用預設的分配器kCFAllocatorDefault。
  2. order:事件源優先順序,當Run Loop中有多個接收相同事件的事件源被標記為待執行時,那麼就根據該優先順序判斷,0為最高優先順序別。
  3. context:事件源上下文。

Run Loop事件源上下文很重要,我們來看看它的結構:

struct CFRunLoopSourceContext { 
    var version: CFIndex 
    var info: UnsafeMutablePointer<Void> 
    var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)! 
    var release: ((UnsafePointer<Void>) -> Void)! 
    var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)! 
    var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)! 
    var hash: ((UnsafePointer<Void>) -> CFHashCode)! 
    var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! 
    var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! 
    var perform: ((UnsafeMutablePointer<Void>) -> Void)! 
    init() 
    init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!) 
}
  1. version:事件源上下文的版本,必須設定為0。
  2. info:上下文中retain、release、copyDescription、equal、hash、schedule、cancel、perform這八個回撥函式所有者物件的指標。
  3. schedule:該回撥函式的作用是將該事件源與給它傳送事件訊息的執行緒進行關聯,也就是說如果主執行緒想要給該事件源傳送事件訊息,那麼首先主執行緒得能獲取到該事件源。
  4. cancel:該回撥函式的作用是使該事件源失效。
  5. perform:該回撥函式的作用是執行其他執行緒或當前執行緒給該事件源發來的事件訊息。
將事件源新增至Run Loop

事件源建立好之後,接下來就是將其新增到指定某個模式的Run Loop中,我們來看看這個方法:

func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
  1. rl:希望新增事件源的Run Loop物件,型別是CFRunLoop。
  2. source:我們建立好的事件源。
  3. mode:Run Loop的模式。
標記事件源及喚醒Run Loop
func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)

func CFRunLoopWakeUp(_ rl: CFRunLoop!)

這裡需要注意的是喚醒Run Loop並不等價與啟動Run Loop,因為啟動Run Loop時需要對Run Loop進行模式、時限的設定,而喚醒Run Loop只是當已啟動的Run Loop休眠時重新讓其執行。

三、執行緒的資源消耗

執行緒的資源消耗主要分為三類,一類是記憶體空間的消耗、一類是建立執行緒消耗的時間、另一類是對開發人員開發成本的消耗。

記憶體空間的消耗又分為兩部分,一部分是核心記憶體空間,另一部分是應用程式使用的記憶體空間,每個執行緒在建立時就會申請這兩部分的記憶體空間。申請核心記憶體空間是用來儲存管理和協調執行緒的核心資料結構的,而申請應用程式的記憶體空間是用來儲存執行緒棧和一些初始化資料的。對於使用者級別的二級執行緒來說,對應用程式記憶體空間的消耗是可以配置的,比如執行緒棧的空間大小等。

下面是兩種記憶體空間通常的消耗情況:

  • 核心記憶體空間:主要儲存執行緒的核心資料結構,每個執行緒大約會佔用1KB的空間。
  • 應用程式記憶體空間:主要儲存執行緒棧和初始化資料,主執行緒在OS X中大約佔8MB空間,在iOS中大約佔1MB。二級執行緒在兩種系統中通常佔大約512KB,但是上面提到二級執行緒在這塊是可以配置的,所以可配置的最小空間為16KB,而且配置的空間大小必須是4KB的倍數。

注意:二級執行緒在建立時只是申請了記憶體程式空間,但還並沒有真正分配給二級執行緒,只有當二級執行緒執行程式碼需要空間時才會真正分配。

四、建立執行緒

說到建立執行緒,就得說說執行緒的兩種型別,Joinable和Detach。Joinable型別的執行緒可以被其他執行緒回收其資源和終止。舉個例子,如果一個Joinable的執行緒與主執行緒結合,那麼當主執行緒準備結束而該二級執行緒還沒有結束的時候,主執行緒會被阻塞等待該二級執行緒,當二級執行緒結束後由主執行緒回收其佔用資源並將其關閉。如果在主執行緒還沒有結束時,該二級執行緒結束了,那麼它不但不會關閉,而且資源也不會被系統收回,只是等待主執行緒處理。而Detach的執行緒則相反,會自行結束關閉執行緒並且有系統回收其資源。

五、執行緒屬性配置

執行緒也是具有若干屬性的,自然一些屬性也是可配置的,在啟動執行緒之前我們可以對其進行配置,比如執行緒佔用的記憶體空間大小、執行緒持久層中的資料、設定執行緒型別、優先順序等。

1、 配置執行緒的棧空間大小

  • Cocoa框架:在OS X v10.5之後的版本和iOS2.0之後的版本中,我們可以通過修改NSThread類的stackSize屬性,改變二級執行緒的執行緒棧大小,不過這裡要注意的是該屬性的單位是位元組,並且設定的大小必須得是4KB的倍數。
  • POSIX API:通過pthread_attr_- setstacksize函式給執行緒屬性pthread_attr_t結構體設定執行緒棧大小,然後在使用pthread_create函式建立執行緒時將執行緒屬性傳入即可。

注意:在使用Cocoa框架的前提下修改執行緒棧時,不能使用NSThread的detachNewThreadSelector: toTarget:withObject:方法,因為上文中說過,該方法先建立執行緒,即刻便啟動了執行緒,所以根本沒有機會修改執行緒屬性。

2、配置執行緒儲存字典

每一個執行緒,在整個生命週期裡都會有一個字典,以key-value的形式儲存著線上程執行過程中你希望儲存下來的各種型別的資料,比如一個常駐執行緒的執行狀態,執行緒可以在任何時候訪問該字典裡的資料。

在Cocoa框架中,可以通過NSThread類的threadDictionary屬性,獲取到NSMutableDictionary型別物件,然後自定義key值,存入任何裡先儲存的物件或資料。如果使用POSIX執行緒,可以使用pthread_setspecific和pthread_getspecific函式設定獲取執行緒字典。

3、配置執行緒型別

在上文中提到過,執行緒有Joinable和Detached型別,大多數非底層的執行緒預設都是Detached型別的,相比Joinable型別的執行緒來說,Detached型別的執行緒不用與其他執行緒結合,並且在執行完任務後可自動被系統回收資源,而且主執行緒不會因此而阻塞,這著實要方便許多。

使用NSThread建立的執行緒預設都是Detached型別,而且似乎也不能將其設定為Joinable型別。而使用POSIX API建立的執行緒則預設為Joinable型別,而且這也是唯一建立Joinable型別執行緒的方式。通過POSIX API可以在建立執行緒前通過函式pthread_attr_setdetachstate更新執行緒屬性,將其設定為不同的型別,如果執行緒已經建立,那麼可以使用pthread_detach函式改變其型別。Joinable型別的執行緒還有一個特性,那就是在終止之前可以將資料傳給與之相結合的執行緒,從而達到執行緒之間的互動。即將要終止的執行緒可以通過pthread_exit函式傳遞指標或者任務執行的結果,然後與之結合的執行緒可以通過pthread_join函式接受資料。

雖然通過POSIX API建立的執行緒使用和管理起來較為複雜和麻煩,但這也說明這種方式更為靈活,更能滿足不同的使用場景和需求。比如當執行一些關鍵的任務,不能被打斷的任務,像執行I/O操作之類。

4、 設定執行緒優先順序

不論是通過NSThread建立執行緒還是通過POSIX API建立執行緒,他們都提供了設定執行緒優先順序的方法。我們可以通過NSThread的類方法setThreadPriority:設定優先順序,因為執行緒的優先順序由0.0~1.0表示,所以設定優先順序時也一樣。我們也可以通過pthread_setschedparam函式設定執行緒優先順序。

注意:設定執行緒的優先順序時可以線上程執行時設定。

雖然我們可以調節執行緒的優先順序,但不到必要時還是不建議調節執行緒的優先順序。因為一旦調高了某個執行緒的優先順序,與低優先順序執行緒的優先等級差距太大,就有可能導致低優先順序執行緒永遠得不到執行的機會,從而產生效能瓶頸。比如說有兩個執行緒A和B,起初優先順序相差無幾,那麼在執行任務的時候都會相繼無序的執行,如果將執行緒A的優先順序調高,並且當執行緒A不會因為執行的任務而阻塞時,執行緒B就可能一直不能執行,此時如果執行緒A中執行的任務需要與執行緒B中任務進行資料互動,而遲遲得不到執行緒B中的結果,此時執行緒A就會被阻塞,那麼程式的效能自然就會產生瓶頸。

六、參考:

  1. Threading Programming Guide

相關文章