本文名爲《GCD 實現同步鎖》,內容不止於鎖。文章試圖經過 GCD 同步鎖的問題,儘可能往外延伸擴展,以講解更多 GCD 同步機制的內容。 html
若是一段代碼所在的進程中有多個線程在同時運行,那麼這些線程就有可能會同時運行這段代碼。假如多個線程每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。或者說:一個類或者程序所提供的接口對於線程來講是原子操做或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題。 linux
因爲可讀寫的全局變量及靜態變量能夠在不一樣線程修改,因此這二者也一般是引發線程安全問題的所在。在 Objective-C 中還包括屬性和實例變量(實際上屬性和實例變量本質上也能夠看作類內的全局變量)。 編程
在 Objective-C 中,若是有多個線程執行同一份代碼,那麼有可能會出現線程安全問題。這種狀況下,就須要一個同步機制來解決 —— 鎖(lock)。在 Objective-C 中,有以下幾種可用的鎖: 安全
- NSLock 實現鎖
NSLock是Cocoa提供給咱們最基本的鎖對象,這也是咱們常常所使用的鎖之一。
.- @synchronized 關鍵字構建的鎖
synchronized指令實現鎖的優勢就是咱們不須要在代碼中顯式的建立鎖對象,即可以實現鎖的機制,但做爲一種預防措施,@synchronized塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。因此若是不想讓隱式的異常處理例程帶來額外的開銷,你能夠考慮使用鎖對象。
.- 使用 C 語言的 pthread_mutex_t 實現的鎖
.- 使用 GCD 來實現的「鎖」
在GCD中也已經提供了一種信號機制,使用它咱們也能夠來構建一把「鎖」。從本質意義上講,信號量與鎖是有區別,具體差別參加信號量與互斥鎖之間的區別。
.- NSRecursiveLock 遞歸鎖
遞歸鎖會跟蹤它被多少次lock。每次成功的lock都必須平衡調用unlock操做。只有全部的鎖住和解鎖操做都平衡的時候,鎖才真正被釋放給其餘線程得到。
.- NSConditionLock 條件鎖
當咱們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能徹底知足咱們的使用。由於普通的鎖只能關心鎖與不鎖,而不在意用什麼鑰匙才能開鎖,而咱們在處理資源共享的時候,多數狀況是隻有知足必定條件的狀況下才能打開這把鎖。
.- NSDistributedLock 分佈式鎖
從它的類名就知道這是一個分佈式的 Lock。NSDistributedLock 的實現是經過文件系統的,因此使用它才能夠有效的實現不一樣進程之間的互斥,但 NSDistributedLock 並不是繼承於 NSLock,它沒有 lock 方法,它只實現了 tryLock,unlock,breakLock,因此若是須要 lock 的話,你就必須本身實現一個 tryLock 的輪詢。
補充:簡單查了下資料,這個鎖主要用於 OS X 的開發。而iOS 較少用到多進程,因此不多在 iOS 上見到過。因爲精力有限,查詢不夠充分,若有錯誤請指出,謝謝!
在 GCD 以前,解決線程安全一般有兩種鎖。一是採用內置的同步鎖 多線程
- (void)synchronizedMethod { @synchronized(self) { // safe code } }
這種寫法會根據給定對象,自動建立一個鎖,並等待塊中的代碼執行完畢,才釋放鎖。這段代碼自己沒什麼問題,可是由於 @synchronized(self) 鎖的對象是 self,形成共用此鎖的同步塊阻塞,下降效率。 併發
// someString 屬性 // 當 someString 開始讀時,對其的寫入阻塞,這是合理的; - (NSString *)someString { @synchronized(self) { return _someString; } } - (NSString *)setSomeString:(NSString *)someString { @synchronized(self) { _someString = someString; } } //otherString 屬性 // 當線程在對 someString 進行讀寫時,與之無關的 otherString 也會受到干擾阻塞,這是不合理的; - (NSString *)otherString { @synchronized(self) { return _otherString; } } - (NSString *)setOtherString:(NSString *)otherString { @synchronized(self) { _otherString = otherString; } }
此例子只用於說明 @synchronized(self) 的問題。聰明的同窗應該還會想到直接使用 atomic 來修飾屬性,進行同步操做更簡單直接。 異步
另外一種方法是使用 NSLock 對象 async
_lock = [[NSLock alloc] init]; - (void)synchronizedMethod { [_lock lock]; //safe code... [_lock unlock]; }
然而 NSLock 有可能在不經意間就形成了死鎖 分佈式
//主線程中 NSLock *theLock = [[NSLock alloc] init]; TestObject *aObject = [[TestObject alloc] init]; //線程1 //線程1 在遞歸的block內,可能會進行屢次的lock,而最後只有一次unlock dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void(^TestMethod)(int); TestMethod = ^(int value) { [theLock lock]; if (value > 0) { [aObject method1]; sleep(5); TestMethod(value-1); } [theLock unlock]; }; TestMethod(5); }); //線程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); [theLock lock]; [aObject method2]; [theLock unlock]; });
這段代碼就是一種典型的死鎖狀況,能夠用遞歸鎖 NSRecursiveLock 來避免這種狀況。使用 NSRecursiveLock 類定義的鎖會跟蹤它被多少次 lock,每次成功的 lock 都必須平衡調用 unlock 操做。只有全部的上鎖和解鎖操做都平衡的時候,鎖才真正被釋放給其餘線程得到。 函數
在講解 GCD 同步機制前,先講點 GCD 的基礎知識。GCD 是異步任務的技術之一,開發者能夠用它將自定義的任務(task)追加到適當的派發隊列(dispatch queue),就能生成必要的線程並執行任務。
在 GCD 中有三種隊列:主隊列(main queue)、全局隊列(global queue)、用戶隊列(user-created queue)。全局隊列是併發隊列,即隊列中的任務(task)執行順序和進入隊列的順序無關;主隊列和用戶隊列是串行隊列,隊列中的任務按FIFO(first input first output,先進先出)的順序執行。
GCD 有兩種派發方式:同步派發和異步派發。千萬注意:這裏的同步和異步指的是 「任務派發方式」,而非任務的執行方式。
看個例子:
// 這小段代碼有問題,出現了線程死鎖,知道爲何嗎? // 提示:下面的代碼在主線程(main_thread)中執行 - (void)viewDidLoad { dispatch_sync(dispatch_get_main_queue(), block()); }
要理解這題,首先須要瞭解 dispatch_sync 和 dispatch_async 的工做流程。
dispatch_sync(queue, block) 作了兩件事情
- 將 block 添加到 queue 隊列;
- 阻塞調用線程,等待 block() 執行結束,回到調用線程。
dispatch_async(queue, block) 也作了兩件事情:
- 將 block 添加到 queue 隊列;
- 直接回到調用線程(不阻塞調用線程)。
這裏也能看到同步派發和異步派發的區別,就是看是否阻塞調用線程。
回到題目,當在 main_thread 中調用 dispatch_sync 時:
- main_thread 被阻塞,沒法繼續執行;
- 同步派發 sync 致使 block() 須要在 main_thread 中執行結束纔會返回;
- 而此時 main_thread 被阻塞,二者互相等待,線程死鎖;
因此記住這個教訓:不要將 block 同步派發到調用 GCD 所在線程的關聯隊列中。例如,若是你在主線程(main thread)中調用 GCD,那麼在 GCD 內就不要使用同步派發(dispatch_sync)將 block 派發到主線程(main thread)關聯的主隊列(main queue)中。
除此以外,還有個容易讓人忽略而致使死鎖的東西:隊列的層級體系。
// 因最外層 queueA 已經同步派發,致使內層 queueA 同步派發時會死鎖 // 這個例子同時也告誡咱們不要相信和使用 dispatch_get_current_queue dispatch_sync (queueA, ^{ dispatch_block_t block = ^{ if (dispatch_get_current_queue() == queueA) { block(); } else { dispatch_sync(queueA, block); } } })
隊列層級用圖畫出來一般長這樣,最頂層是全局併發隊列(此圖和上面例子無關)
有了前面的基礎,就能夠瞧瞧在 GCD 中更好的同步鎖的實現方式。在 GCD 隊列中,有個簡單直接的方法能夠代替同步鎖或鎖對象,將讀寫操做都安排在一個串行同步隊列裏,便可保證數據同步,以下:
_syncQueue = dispatch_queue_create("com.effectiveObjectiveC.syncQueue", NULL); - (NSString *)someString { __weak NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString *)someString { dispatch_sync(_syncQueue, ^{ _someString = someString; }); }
使用串行同步隊列,將讀寫操做所有放在序列化的隊列裏執行,全部指針對屬性的操做便可同步。加鎖和解鎖的所有轉移給 GCD 處理,而 GCD 在較深的底層實現,能夠進行許多的優化。
然而設置方法不必定非得是同步的,設置實例變量的 block 沒有返回值,因此能夠將此方法改爲異步:
- (void)setSomeString:(NSString *)someString { dispatch_async(_syncQueue, ^{ _someString = someString; }); }
此次只是把 dispatch_sync 改爲 dispatch_async,從調用者來看提高了執行速度。但正是因爲執行異步派發
dispatch_async 時會拷貝 block,當拷貝 block 的時間大於執行 block 的時間時,dispatch_async 的速度會比 dispatch_sync 速度更慢。因此實際狀況應根據 block 所執行任務的繁重程度來決定使用 dispatch_async 仍是 dispatch_sync。
多個獲取方法能夠併發執行,獲取方法與設置方法不能併發執行。據此可使用併發隊列和 GCD 的 barrier 來寫出更快的代碼。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - (NSString *)someString { __weak NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString *)someString { // barrier dispatch_barrier_async(_syncQueue, ^{ _someString = someString; }); }
在使用上面的方式建立的同步鎖以後,會發現執行速度和效率都更高。難道併發隊列厲害嗎?其實緣由不僅是併發隊列,還有 barrier block 的功勞,那麼什麼是 barrier block 呢?
函數 dispatch_barrier_sync 和 dispatch_barrier_async 可讓隊列中派發的 block 變成 barrier(柵欄) 使用,這種 block 稱爲 barrier block。隊列中的 barrier block 必須等當前併發隊列中的 block 都執行結束纔開始執行,時序圖以下:
GCD 的同步方式還有組派發(dispatch group)和信號量(dispatch semaphore)
/** * 阻塞當前線程,執行group內任務,阻塞時間爲timeout * * @param group 等待的group * @param timeout 等待的時間,即函數在等待dispatch group內的任務執行完畢時,應阻塞多久 * * @return 若是執行dispatch group所需時間小於timeout,則返回0,不然返回非0值; timeout能夠取常量DISPATCH_TIME_FOREVER,表示永遠不會超時 */ long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
/** * 若是group內的任務所有執行完畢後,將block提交到queue上執行 * * @param group 等待的group * @param queue 即將提交的隊列 * @param block 即將提交的任務 */ void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
信號量在 linux/unix 開發中十分常見,其概念至關於經典的「生產者-消費者」模型。當信號個數爲 0 時,則線程阻塞,等待發送新信號;一旦信號個數大於 0 時,就開始處理任務。
dispatch_semaphore_create:建立一個semaphore
dispatch_semaphore_signal:發送一個信號,信號個數加1
dispatch_semaphore_wait:等待信號
除了《Effective Objective-C 2.0》以外,本文還參考了:
[0] 百度百科:線程安全的基本概念
[1] 老譚:Objective-C 中不一樣方式實現鎖(一)
[2] 老譚:Objective-C 中不一樣方式實現鎖(二)
[4] 老譚:在 CGD 中快速實現多線程的併發控制
[5] 飄飄白雲:深刻淺出 Cocoa 多線程編程之 block 與 dispatch quene