第46条:不要使用 dispatch_get_current_queue
本條要點:(作者總結)
- ?dispatch_get_current_queue 函數對行為常常與開發者所預期的不同。此函數已經廢棄、止應做調試之用。
- 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
- dispatch_get_current_queue 函數用于解決由不可重入的代碼所引發的死鎖,然而能用函數解決的問題,通常也能改用“隊列特定數據”來解決。
使用 GCD 時,經常需要判斷當前代碼正在哪個隊列上執行,向多個隊列派發任務時,更是如此。例如,Mac OS X 與 iOS 的 UI 事務都需要在主線程上執行,而這個線程就相當于 GCD 中的主隊列。有時似乎需要判斷出當前代碼是不是在主隊列上執行。閱讀開發文檔時,大家會發現下面這個函數:
1 dispatch_queue_t dispatch_get_current_queue()文檔中說,此函數返回當前正在執行代碼的隊列。確實是這樣,不過用的時候要小心。實際上,iOS 系統從 6.0 版本起,已經正式棄用此函數了。不過 Mac OS X 系統直到 10.8 版本也尚未將其廢棄。雖說如此,但在 Mac OS X 系統里還是要避免使用它。
該函數有種典型的錯誤用法(antipattern, “反模式”),就是用它檢測當前隊列是不是某個特定的隊列,試圖以此來避免執行同步派發時可能遭遇的死鎖問題。考慮下面這兩個存取方法,其代碼用隊列來證實對實例變量的訪問操作是同步的:
1 - (NSString *)someString { 2 3 __block NSString *localSomeString; 4 dispatch_sync(_syncQueue, ^{ 5 6 localSomeString = _someString; 7 }); 8 return localSomeString; 9 } 10 11 12 13 - (void)setSomeString:(NSString *)someString { 14 15 dispatch_async(_syncQueue, ^{ 16 17 _someString = someString; 18 }); 19 }這種寫法的問題在于,獲取方法(getter)可能會死鎖,假如調用獲取方法的隊列恰好是同步操作所針對的隊列(本例中是 _syncQueue),那么 dispatch_sync 就一直不會返回,直到塊執行完畢為止。可是,應該執行塊的那個目標隊列卻是當前隊列,而當前隊列的 dispatch_sync 又一直阻塞著,它在等待目標隊列把這個塊執行完,這樣一來,塊就永遠沒機會執行了。像 someString 這種方法,就是 “不可重入的”。
看了 dispatch_get_current_queue 的文檔后,你也許覺得可以用它改寫這個方法,令其變得“可重入”,只需檢測當前隊列是否為同步操作所針對的隊列,如果是,就不派發了,直接執行塊即可:
1 - (NSString *)someString { 2 3 __block NSString *localSomeString; 4 dispatch_block_t accessorBlock = ^ { 5 localSomeString = _someString; 6 }; 7 if (dispatch_get_current_queue() == _syncQueue) { 8 accessorBlock(); 9 } else { 10 dispatch_sync(_syncQueue, accessorBlock); 11 } 12 13 return localSomeString; 14 }這些做法可以處理一些簡單的情況。不過仍然有死鎖的危險。為說明原因,請讀者考慮下面這段代碼,其中有兩個串行派發隊列:
1 dispatch_queue_t queueA = dispatch_queue_create("com.efftiveobjectivec.queueA", NULL); 2 3 dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL); 4 5 dispatch_sync(queueA, ^{ 6 7 dispatch_sync(queueB, ^{ 8 9 dispatch_sync(queueA, ^{ 10 11 // Deadlock 12 }); 13 }); 14 15 });這段代碼執行到最內層的派發操作時,總會死鎖,因為此操作是針對 ?queueA 隊列的,所以必須等最外層的 dispatch_sync 執行完畢才行(因為最外層的派發操作與最內層一樣,也是針對 queueA 的),而最外層的那個 dispatch_sync 又不可能執行完畢,因為它要等最內層的 dispatch_sync 執行完,于是就死鎖了。現在按照剛才的辦法,使用 dispatch_get_current_queue 來檢測:
1 dispatch_sync(queueA, ^{ 2 3 dispatch_sync(queueB, ^{ 4 5 dispatch_block_t block = ^{/*...*/}; 6 if (dispatch_get_current_queue() == queueA) { 7 block(); 8 } else { 9 dispatch_sync(queueA, block); 10 } 11 12 }); 13 14 });然而這樣做依然死鎖,因為 dispatch_get_current_queue 返回的是當前隊列,在本例中就是 queueB。這樣的話,針對queueA 的同步派發操作依然會執行,于是和剛才一樣,還是死鎖了。
在這種情況下,正確做法是:不要把存取方法做成可重入的,而是應該確保操作同步操作所用的隊列絕不會訪問屬性,也就是絕對不會調用 someString 方法。這種隊列只應該用來同步屬性。由于派發隊列是一種極為輕量的機制,所以,為了確保每項屬性都有專用的同步隊列,我們不妨創建多個隊列。
剛才那個例子似乎稍顯做作,但是使用隊列時還要注意另外一個問題,而那個問題會在你意想不到的地方導致死鎖。隊列之間會形成一套層級體系,這意味著排在某條隊列中的塊,會在其上級隊列(parent queue,也叫“父隊列”)里執行。層級里地位最高的那個隊列總是 “全局并發隊列”(global concurrentqueue)圖描繪了一套簡單的隊列體系。
排在隊列B或隊列C中的塊,稍后會在隊列A里依序執行。于是,排在隊列A、B、C 中的塊總是要彼此錯開執行。然而,安排在隊列D 中的塊,則有可能與隊列A 里的塊(也包括隊列B 與 隊列C 里的塊)并行,因為A 與 D 的目標隊列是個并發隊列。若有必要,并發隊列可以用多個線程并行執行多個塊,而是否會這樣做,則需要根據 CPU 的核心數量等系統資源狀況來定。
? 由于隊列間有層級關系,所以 “檢查當前隊列是否為執行同步派發所用的隊列”這種辦法,并不總是奏效。比方說,排在隊列C里的塊,會認為當前隊列就是隊列C,而開發者可能據此認定:在隊列A上能夠安全的執行同步派發操作。但實際上,這么做依然會像前面那樣導致死鎖。
有的 API 可令開發者指定運行回調塊時所用的隊列,但實際上卻會把回調塊安排在內部的串行隊列上,而內部隊列的目標隊列又是開發者所提供的那個隊列,在此情況下,也許就要出現剛才說的那種問題了。使用這種 API 的開發者可能誤以為:在回調塊里調用 dispatch_get_current_queue 所返回的 “當前隊列”,總是其調用API時指定的那個。但實際上返回的卻是API內部的那個同步隊列。
要解決這個問題,最好的辦法就是通過 GCD 所提供的功能來設定“隊列特有數據”(queue-specific data),此功能可以把任意數據以鍵值對的形式關聯到隊列里。最重要之處在于,假如根據指定的鍵獲取不到關聯數據,那么系統就會沿著層級體系向上查找,直至找到數據或到達根隊列為止。筆者這么說,大家也許還不太明白其用法,所以看下面這個例子:
1 dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL); 2 3 dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL); 4 5 dispatch_set_target_queue(queueB, queueA); 6 7 8 9 static int kQueueSpecific; 10 11 CFStringRef queueSpecificValue = CFSTR("queueA"); 12 13 dispatch_queue_set_specific(queueA, &kQueueSpecific, (void*)queueSpecificValue,(dispatch_function_t)CFRelease); 14 15 dispatch_sync(queueB, ^{ 16 17 dispatch_block_t block = ^{ NSLog(@"No deadlock!");}; 18 CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); 19 if (retrievedValue) { 20 block(); 21 } else { 22 23 dispatch_sync(queueA, block); 24 } 25 26 });本例創建了兩個隊列。代碼中將隊列B的目標隊列設為隊列A,而隊列A的目標隊列仍然是默認優先級的全局并發隊列。熱后使用下列函數,在隊列A上設置“隊列特定值”:
1 void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);此函數的首個參數表示待設置數據隊列,其后面兩個參數是鍵與值。鍵與值都是不透明的void 指針。對于鍵來說,有個問題一定要注意:函數是按指針值來比較鍵的,而不是按照其內容。所以,“隊列特定數據”的行為與 NSDictionary 對象不同,后者是比較鍵的 “對象等同性”。“隊列特定數據”更像是關聯引用。值(在函數原型里叫做 “context”(中文稱為“上下文”、“語境”、“環境參數”等))也是不透明的void 指針,于是可以在其中存放任意數據。然而,必須管理該對象的內存。這使得在ARC 環境下很難使用Objective-C 對象作為值。范例代碼使用 coreFoundation 字符串作為值,因為ARC 并不會自動管理CoreFoundation 對象的內存。所以說,這種對象非常適合充當“隊列特定數據”,它們可以根據需要與相關的Objective-C Foundation 類無縫銜接。
函數的最后一個參數是“析構函數”(destructor function),對于給定的鍵來說,當隊列所占內存為系統所回收,或者有新的值與鍵相關聯時,原有的值對象就會移除,而析構函數也會于此時運行。dispatch_function_t 類型的定義如下:
1 typedef void (*dispatch_function_t) (void *)由此可知,析構函數只能帶有一個指針參數且返回值必須為 void。范例代碼采用 CFRelease 做析構函數,此函數符合要求,不過也可以采用開發者自定義的函數,在其中調用 CFRelease 以清理舊值,并完成其他必要的清理工作。
于是,“隊列特定數據”所提供的這套簡單易用的機制,就避免了使用 dispatch_get_current_queue 時經常遭遇的一個陷阱。此外,調試程序時也許會經常用到 dispatch_get_current_queue。在此情況下,可以放心的使用這個已經廢棄的方法,只是別把它編譯到發行版本的程序里就行。如果對“訪問當前隊列” 這項操作有特殊需求,而現有函數又無法滿足,那么最好還是聯系蘋果公司,請求其加入此功能。
END
轉載于:https://www.cnblogs.com/chmhml/p/7421050.html
總結
以上是生活随笔為你收集整理的第46条:不要使用 dispatch_get_current_queue的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cat命令汇总整理
- 下一篇: It is possible that