多執行緒
執行緒的基本概念
執行緒 (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,選擇加入一起交流,一起學習,期待你的加入!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/4164.html
標籤:iOS
上一篇:iOS閃退日志的收集和決議
下一篇:iOS性能優化
