多線程 線程的基本概念 線程 (thread)是進程(process)A 內假想的持有 CPU 使用權的執行單位。一般情況下,一個進程 只有一個線程,但也可以創建多個線程併在進程中並行執行。應用在執行某一處理的同時,還可以 接收 GUI 的輸入。 使用多線程的程式稱為 多線程 (multithrea ...
多線程
線程的基本概念
線程 (thread)是進程(process)A 內假想的持有 CPU 使用權的執行單位。一般情況下,一個進程 只有一個線程,但也可以創建多個線程併在進程中並行執行。應用在執行某一處理的同時,還可以 接收 GUI 的輸入。
使用多線程的程式稱為 多線程 (multithread)運行。從程式開始執行時就運行的線程稱為 主線程 , 除此之外,之後生成的線程稱為次線程(secondary thread)或子線程(subthread)。
創建線程時,創建方的線程為父線程,被創建方的線程為子線程。父線程和子線程並行執行各
自的處理,但父線程可以等到子線程執行終止後與其會合(join)。而另一方面,線上程被創建後, 也可以切斷父子關係指定它們不進行會合。該操作稱為 分離 (detach)。這裡所說的 NSThread 就是在 分離狀態下創建線程。
一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿裡面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。
由於被創建的線程共用進程的地址空間,所以能夠自由訪問進程的空間變數。多線程訪問的變數稱為 共用變數 (shared variable) 。共用變數大多為全局變數或靜態變數,但因為地址空間是共用的, 所以理論上所有記憶體區域都可以稱為共用變數。
如果多線程胡亂訪問共用變數,那麼就不能保證變數值的正確性。所以有時就需要按照一定的 規則使多線程可以協調動作。此時就必須執行線程間 互斥 (或者排他控制,mutual exclusion)(見 。 各個線程都分配有棧且獨立進行管理。基本上不能訪問其他線程的棧內的變數(自動變數)。通 過遵守這樣的編程方式,就可以自由訪問方法或函數的自動變數,而且不用擔心互斥。
使用引用計數管理方式時,為了使對象之間解耦合,子線程方需要創建與父線程不同的自動釋
放池來管理。使用垃圾回收時不需要這樣。
A 任務(task)這一名稱也被用來表示與進程同樣的概念,在蘋果公司的文檔“Multithreading Programming Topics”中, 可以包含多線程的程式的執行單元稱為進程,而任務則被用來抽象地表示應該進行的作業。
線程安全
多個線程同時操作某個實例時,如果沒有得到錯誤結果或實例沒有包含不正確的狀態,那麼該 類就稱為 線程安全 (thread-safe)。結果不能保證時,則稱為非線程安全或線程不安全(thread-unsafe)。
一般情況下,常數對象是線程安全的,變數對象不是線程安全的。常數對象可以線上程間安全
地傳遞,但對變數對象共用時,需要恰當地執行互斥或同步切換。
需要註意的是 C 語言的函數。就現狀來看,BSD 函數的大部分,例如 printf() 等,都不是線程安 全的。
註意點
在某些情況下,使用多線程可以使處理高速化、實現易於使用的介面、使實現更簡單等。但並 不是說使用多線程後就一定會得到這些優點。
要想使多線程程式不出錯且高效執行,並行編程的知識必不可少。線程間的任務分配和信息交 換、共用資源的互斥、與 GUI 的交互及動畫顯示等,在使用時都要特別小心。
一般情況下,自己實現多線程程式是很困難的,而且也容易埋下高隱患。稍有差錯或設計失誤,
多 線 程 便 不 能 發 揮 效 果, 甚 至 還 會 導 致 未 知 原 因 的 釋 放 或 異 常 終 止。 使 用 19.3 節 中 介 紹 的 NSOperation,雖然可以較容易地實現多線程程式,但是也必須掌握線程動作、互斥等相關知識。不 能適應這些的讀者建議去參考一下並行編程的相關書籍。
而且,很多多線程中遇見的問題都可以通過 NSTimer 類或延遲消息發送(參考 15.1 節)來解決。 大家也不妨嘗試一下用這些方法來解決相關問題。
使用 NSThread創建線程
Foundation 框架中提供了 NSThread 類來創建並控制線程。該類的介面在 Foundation/NSThread.h 中聲明。
創建新線程需要執行下麵的類方法。
+ (void) detachNewThreadSelector: (SEL) aSelector
toTarget: (id) aTarget
withObject: (id) anArgument
對 對象 aTarget 調用方法創建新線程並執行。選擇器 aSelector 必須是僅獲取一個 id 類型參數且返回值 為 void 的執行方法(參考 8.2 節)。
指定的方法執行結束後,線程也隨之終止。線程從最初就被執行了分離,所以終止時沒有和父 線程會合。當主線程終止時,包含子線程的程式也全部隨之終止。
使用引用計數管理(手動及 ARC)時,有時需要執行的方法自身來管理自動釋放池。此外,參 數 aTarget 和 anArgument 中指定的對象也與線程同時存在,即在創建線程時被保存,線上程終止時 被釋放。 使用下述的 NSApplication 類中的方法也能創建線程。該方法使用上面的方法,而且在使用引用 計數管理時還會創建線程的自動釋放池。
+ (void) detachDrawingThread: (SEL) selector
toTarget: (id) target
withObject: (id) argument
創建新線程並執行的方法除了上述方法還有很多,本書中不再一一介紹。其他方法請參考 NSThread、NSObject 的參考文檔。
程式可以調用 NSThread 類方法來確認是否是多線程運行。 + (BOOL) isMultiThreaded 多個線程並行執行時或者只有主線程在執行時,只要在此之前已經創建了線程,則返回 YES。
當前線程
一個線程稱自身為 當前線程 (current thread),區別於其他線程。
子線程將創建時指定的方法執行完後也會隨之終止,但也可以中途終止。為此,可以使用當前 線程(線程自身)來執行下一個 NSThread 類方法。但是,使用引用計數管理時,終止前一定要釋放 自動釋放池。 + (void) exit
使用下述方法獲得表示線程的 NSThread 實例。
+ (NSThread *) currentThread
獲 得表示當前線程的 NSThread 實例。
+ (NSThread *) mainThread
獲 得表示主線程的NSThread實例。查看當前線程是否為主線程時,可以使用類方法isMainThread 。 每個線程都可以持有一個該線程固有的 NSMutableDictionary 類型的字典。向 NSThread 實例發 送下麵的消息類就可以取得字典。
- (NSMutableDictionary *) threadDictionary
可以使當前線程僅被中斷幾秒。為此,可在當前線程中執行下麵的類方法。參數為實數。 + (void) sleepForTimeInterval: (NSTimeInterval) ti
也可以使線程在某一時刻前中斷,這時可採用下麵的類方法。參數是表示日期的類 NSDate 實例。 + (void) sleepUntilDate:(NSDate *) aDate
如果要使線程到某個條件成立前一直保持休眠狀態,則要使用下一章節介紹的鎖。
GUI應用和線程
在使用 GUI 的應用中,事件處理和繪圖等大部分處理中線程都發揮了重要作用。也可以在子線 程中創建窗體,或分擔部分繪圖功能,但要註意避免競爭或記憶體泄漏。詳情請參考相關文檔。
GUI 應用中有較容易的方法來使用線程,即將 GUI 相關的時間處理或繪圖集中在主線程中進行。
使用下麵的方法,就可以從子線程依賴主線程中的方法處理。該方法為 NSOjbect 的範疇,在頭文件 Foundation/NSThread.h 中聲明。
- (void) performSelectorOnMainThread: (SEL) aSelector
withObject: (id) arg
waitUntilDone: (BOOL) wait
選 擇器 aSelector 和參數 arg 中指定的方法的執行依賴於主線程。wait 為 YES 時,當前線程會一直等待 至執行結束。主線程中必須有事件迴圈(運行迴路)。
互斥
需要互斥的例子
在多線程環境中,無論哪個函數或方法都可以在多線程中同時執行。但是,在使用共用變數時, 或者在執行文件輸出或繪圖等的情況下,多線程同時執行就可能得到奇怪的結果。
例如,使用整數全局變數 totalNumber 來累加所處理的數據的個數。為了執行下麵的加法計算,
在多線程環境中執行該方法會得到什麼結果呢?
- (void)addNumber:(NSIngeger)n
{
totalNumber += n;
} 在 OS 功能支持下,線程在運行的過程中會時而得到 CPU 的執行權,時而被掛起執行權,2 個 方法的執行情況如圖 19-1 中所示。在該圖中,線程 1 將新計算的值保存在寄存器時掛起 CPU 執行 權,同時線程 2 開始執行方法。即使 CPU 的執行權被掛起,寄存器的值也仍然可以被保存,所以各 線程都能正常處理。但是,由於線程 2 寫入的值消失了,因此整體上看,這偏離了我們期待的結果。 原因是值的讀取、更新、寫入操作被多線程同時執行了。
在圖 19-1 的例子中,我們將同時只可以由一個線程占有並執行的代碼部分稱為臨界區(critical section),或稱為危險區。互斥的目的就是限制可以在臨界區執行的線程。
鎖
為了使多個線程間可以相互排斥地使用全局變數等共用資源,可以使用NSLock 類。該類的實例 也就是可以調整多線程行為的 信號量 (semaphore)或者 互斥型信號量 (mutual exclusion semaphore)。 Cocoa 環境中也稱為 鎖 (lock)。
鎖具有每次只允許單個線程獲得並使用的性質。獲得鎖稱為“加鎖”,釋放鎖稱為“解鎖”。
鎖和普通的實例一樣,使用類方法alloc 和初始化器init 來創建並初始化。但是,鎖應該在程 序開始在多線程執行前創建。
NSLock *countLock = [[NSLock alloc] init];
獲得鎖的方法和釋放(unlock)鎖的方法都在協議 NSLocking 中定義。
- (void) lock 如果鎖正被使用,則線程進入休眠狀態。
如果鎖沒有被使用,則將鎖的狀態變為正被使用,線程繼續執行。
- (void) unlock 將 鎖置為沒有在被使用,此時如果有等待該鎖資源的正在休眠的線程,則將其喚醒。
在上例中,使用鎖後會產生如下效果。但需要預先創建 NSLock 的實例 aLock。在該代碼中,從 某線程執行 A 取得鎖到該線程執行 B 釋放鎖期間,其他線程在執行 A 時將進入休眠狀態,不能執 行臨界區代碼。鎖被釋放後,在執行 A 時休眠的線程中選擇一個線程,該線程在取得鎖後進入臨界 區執行。
- (void)addNumber:(NSIngeger)n { [aLock lock]; ───────────────────────────────────────── A totalNumber += n; // 臨界區 [aLock unlock]; ──────────────────────────────────────── B }
某個鎖被lock 後,必須執行一次unlock 。而且lock 和unlock 必須在同一個線程執行 A。
下麵來看另外一個使用鎖的例子。考慮一下全局變數值自增時返回其結果的方法。多線程執行 時,全局變數 theCount 若想正確地自增,就需要使用鎖 countLock 來管理。
可以採用如下定義。
A lock 和 unlock 必須在同一個線程中執行,因為 NSLock 是基於 POSIX 線程實現的。
死鎖
線程和鎖的關係必須在設計之初就經過仔細的考慮。如果錯誤地使用鎖,不但不能按照預期執 行互斥,還可能使多個線程陷入到不能執行的狀態,即死鎖(deadlock)狀態。
死鎖就是多線程(或進程)永遠在等待一個不可能實現的條件而無法繼續執行,如圖 19-2 所示。
線程 1 占有文件 A 並正在進行處理,途中又需要占有文件 B。而另一方面,線程 2 占有著文件 B,途中又需要占有文件 A。大家不妨設想一下,如果線程 1 和線程 2 同時執行到了圖中的箭頭位置 會怎麼樣呢?線程 1 為了處理文件 B 想要獲得鎖 lockForB,但是它已經被線程 2 獲得。同樣,線程 2 想要獲得的鎖 lockForA 也被線程 1 占有著。這種情況下,線程 1 和線程 2 就會同時進入休眠狀態, 而且雙方都不能跳出該狀態。
像這樣,當多個線程互相等待資源的釋放時,就非常容易出現死鎖現象。有時是多個線程相干預,有時則是一個線程因為自己需要獲得鎖而進入休眠狀態。此外,由於多數情況下各個線程本身 並沒有錯誤處理,而且死鎖又隨時可能發生,因此追究原因就非常困難,也不能排除導致程式 bug 的可能。
嘗試獲得鎖
NSLock 類不僅能獲得鎖和釋放鎖,還有檢查是否能獲得鎖的功能。利用這些功能,就可以在不 能獲得鎖時進行其他處理。
- (BOOL) tryLock
用 接收器嘗試獲得某個鎖,如果可以取得該鎖則返回 YES。不能獲得時,與lock 處理不同,線程沒 有進入休眠狀態,而是直接返回 NO 並繼續執行。
該方法十分便利,但要確保只能在可以獲得鎖時才執行 unlock,創建程式時必須註意這一點。
條件鎖
類 NSConditionLock 稱為 條件鎖 (condition lock)。該鎖持有整數值,根據該值可以獲得鎖或者 等待。
- (id) initWithCondition: (NSInteger) condition
NSConditionLock 實例初始化,設置參數 condition 指定的值。
NSCondtionLock 的指定初始化器。
- (NSInteger) condition
此時返回鎖中設定的值。
- (void) lockWhenCondition: (NSInteger) condition
如果鎖正在被使用,則線程進入休眠狀態。
鎖不在被使用時,如果鎖值和參數 condition 的值一致,則將鎖狀態修改為正在被使用,然後繼續執 行,如果不一致,則線程進入休眠狀態。
- (void) unlockWithCondition: (NSInteger) condition
在鎖中設置參數 condition 指定的值。將鎖設置為不在被使用,此時如果有等待獲得該鎖且處於休眠 狀態的線程,則將其喚醒。
- (BOOL) tryLockWhenCondition: (NSInteger) condition
尚未使用鎖且鎖值與參數 condition 相同時,獲得鎖並返回 YES。不能獲得鎖時也不進入休眠狀態, 而是返回 NO,線程繼續執行。
使用方法 lock 、 unlock 或 tryLock 都可以獲得鎖和釋放鎖,而且無需關心鎖的值。
然而,由於 NSConditionLock 實例可以持有的狀態為整數型,所以事先用枚舉常數或巨集定義就可 以了。如果只使用 0 或 1,不僅不容易理解,也可能造成錯誤。
NSRecursiveLock
某線程獲得鎖後,到該線程釋放鎖期間,想要獲得該鎖的線程就會進入休眠。使用類 NSLock 的 鎖時,如果已經獲得鎖的線程在沒有釋放它的情況下還想再次獲得該鎖,該線程也會進入休眠狀態。 但是,由於沒有從休眠狀態喚醒的線程,所以這就是死鎖。下麵是一個簡單的例子,這段代碼不會 執行。
[aLock lock];
[aLock lock]; // 這裡發生死鎖
[aLock unlock];
[aLock unlock];
解決這種情況可以使用 NSRecursiveLock 類的鎖,擁有鎖的線程即使多次獲得同一個鎖也不會 進入死鎖。但是,其他線程當然也不能獲得該鎖。獲得次數和釋放次數一致時,鎖就會被釋放。
NSRecursiveLock 類的鎖使用起來十分方便,但排除被重覆加鎖的情況,用 NSLock 來重新記述
的話,性能則會更好。
@synchronized
程式內的塊可以指定為不被多線程同時使用。為此可以使用 @synchronized 編譯符,如下所示。
通過使用該段代碼,運行時系統就可以創建排斥地執行該代碼塊的鎖(mutex)。參數 obj 通常指 定為該互斥鎖要保護的對象。obj 自己不需要是鎖對象。
線程如果要執行該代碼塊,首先會嘗試獲得鎖,如果能獲得鎖則可以執行塊內代碼。塊執行結 束時一併釋放鎖。使用 break 或 return 從塊內跳出到塊外時也被視作塊執行終止。而且,在塊內發生 異常時,運行時系統會捕捉異常並釋放塊。
@synchronized 的參數對象決定對應的塊。所以, 同一個對象參數的 @synchronized 塊如果有多 個,則不可以同時執行。
根據參數的選擇方法的不同,@synchronized 會在並行執行的受限對象和可以執行的普通對象之 間動態切換。下麵展示 @synchronized 參數的使用示例。
(a) 是指定只能單獨存在的對象時的情景。同一個對象在其他地方也作為 @synchronized 的參數 使用時,所有這些塊不能同時執行。(b) 也是一樣,因為限制了參數的使用範圍,互斥對象顯然只能 是該方法內的塊。
(c) 是各個實例互斥的例子。一個實例一次只能執行一個線程,同一類別的其他實例則多個線程可以同時存在。(d) 在參數對象可能在多個地方更改的情況下有效,但以同樣方式使用該對象的所有 場所中都需要按照該方式書寫,否則就沒有任何意義。
而且,也可以按照 (e) 的方式書寫。此外還可以指定類對象,或者使用消息選擇器(隱藏參數的 _cmd)來指定方法等。不過一般情況下,為互斥的對象使用專門的鎖對象是比較可靠的方法。
使用 @synchronized 塊時,加鎖和解鎖必須成對進行,因此可以防止加鎖後忘記解鎖這種問題的 發生。和普通的鎖相比,複雜的並行演算法的書寫會較為複雜,但多數情況下都會使互斥更容易理解。
另外,如果你想一起進階,不妨添加一下交流群1012951431,選擇加入一起交流,一起學習。期待你的加入!