41.多用派發隊列,少用同步鎖
42.多用GCD,少用performSelector系列方法
43.掌握GCD及操作隊列的使用時機
44.通過Dispatch Group機制,根據系統資源狀況來執行任務
45.使用dispatch_once來執行只需要運行一次的線程安全代碼
46.不要使用dispatch_get_current_queue
在Objective-C中,如果有多個線程要執行同一份代碼,那麼有時可能會出問題。這種情況下通常要使用鎖來實現某種同步機制。在GCD出現之前,有兩種方法:
// 使用內置同步塊@synchronized - (void)synchronizedMethod{ @synchronized(self){ // Safe } } // 使用NSLock對象 _lock = [[NSLock alloc] init]; - (void)synchronizedMethod{ [_lock lock]; // Safe [_lock unlock]; }
這兩種方法都很好,但都有缺陷。同步塊會降低代碼效率,如在本例中,在self對象上加鎖,程序可能要等另一段與此無關的代碼執行完畢,才能繼續執行當前代碼。而直接用鎖對象的話,一旦遇到死鎖就會非常麻煩。
替代方案就是使用GCD,下面以開發者自己實現的原子屬性的存取方法為例:
// 用同步塊實現 - (NSString*)someString{ @synchronized(self){ return _someString; } } - (void)setSomeString:(NSString*)someString{ @synchronized(self){ _someString = someString; } } // 用GCD實現 // 創建一個串行隊列 _syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NUll); - (NSString*)someString{ __block NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString*)someString{ dispatch_sync(_syncQueue, ^{ _someString = someString; }); }
假如有很多屬性都使用了@synchronized(self),那麼每個屬性的同步塊都要等其他所有同步塊執行完畢後才能執行,且這樣做未必能保證線程安全,如在同一個線程上多次調用getter方法,每次獲取到的結果未必相同,在兩次訪問操作之間,其他線程可能會寫入新的屬性值。
本例GCD代碼采用的是串行同步隊列,將讀取操作及寫入操作都安排在同一個隊列中,可保證數據同步。
可以根據實際需求進一步優化代碼,例如,讓屬性的讀取操作都可以並發執行,但是寫入操作必須單獨執行的情景:
// 創建一個並發隊列 _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - (NSString*)someString{ __block NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString*)someString{ // 將寫入操作放入異步柵欄塊中執行 // 注:barrier表示柵欄,並發隊列如果發現接下來需要處理的塊為柵欄塊,那麼就會等待當前並發塊都執行完畢後再單獨執行柵欄塊,執行完柵欄塊後再以正常方式繼續向下處理。 dispatch_barrier_async(_syncQueue, ^{ _someString = someString; }); }
Objective-C本質上是一門非常動態的語言,NSObject定義了幾個方法,令開發者可以隨意調用任何方法,這些方法可以推遲執行方法調用,也可以指定運行方法所有的線程。其中最簡單的就是performSelector:
- (id)performSelector:(SEL)selector // performSelector方法與直接調用選擇子等效,所以以下兩行代碼執行效果相同 [object performSelector:@selector(selectorName)]; [object selectorName];
如果選擇器是在運行期決定的,那麼就能體現出performSelector的強大,等於在動態綁定之上再次實現了動態綁定。
SEL selector; if(/* some contidion */){ selector = @selector(foo); }else{ selector = @selector(bar); } [object performSelector:selector];
但是這樣做的話,在ARC下編譯代碼,編譯器會發出警告,提示performSelector可能會導致內存洩漏。原因在於編譯器並不知道將要調用的選擇器是什麼。所以沒辦法運用ARC的內存管理規則來判定返回的值是不是應該釋放。鑒於此,ARC采取比較謹慎的操作,即不添加釋放操作,在方法返回對象時已將其保留的情況下會發生內存洩漏。
performSelector系列方法的另一個局限性在於最多只能接受兩個參數,且接受參數類型必須為對象。下面是performSelector系列的常用方法:
// 延遲執行 - (id)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay // 由某線程執行 - (id)performSelector:(SEL)selector onThread:(NSThread*)thread withObject:(id)argument waitUntilDone:(BOOL)wait // 由主線程執行 - (id)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait
這些方法都可以用GCD來替代其功能:
// 延遲執行方法 // 使用performSelector [self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0]; // 使用GCD dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)); dispatch_after(time, dispatch_get_main_queue(), ^(void){ [self doSomething]; }); // 在主線程中執行方法 // 使用performSelector [self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO]; // 使用GCD 如果waitUntilDone為YES,則用dispatch_sync dispatch_async(dispatch_get_main_queue(),^{ [self doSomething]; });
GCD技術確實很棒,不過有時候采用標准系統庫的組件,效果會更好。GCD技術的同步機制非常優秀,且對於那些只需執行一次的代碼來說,使用GCD最方便。但在執行後台任務時,還可以使用操作隊列(NSOperationQueue)。
兩者的差別很多,最大的區別在於,GCD是純C的API,而操作隊列是Objective-C的對象。GCD中,任務用塊來表示,是一個輕量級的數據結構,而操作(NSOperation)則是個更為重量級的Objective-C對象。需要更具對象帶來的開銷和使用完整對象的好處來權衡使用哪種技術。
操作隊列的優勢:
1. 直接在NSOperation對象上調用cancel方法即可取消操作,而GCD一旦分派任務就無法取消。
2. 可以指定操作間的依賴關系,使特定操作必須在另一個操作執行完畢後方可執行。
3. 可以通過KVO(鍵值觀察)來監控NSOperation對象的屬性變化(isCancelled,isFinished等)
4. 可以指定操作的優先級
5. 可以通過重用NSOperation對象來實現更豐富的功能
想要確定哪種方案最佳,最好還是測試一下性能。
dispatch group是GCD的一項特性,能夠把任務分組。調用者可以等待這組任務執行完畢,也可以在提供回調函數之後繼續往下執行,這組任務完成時,調用者會得到通知。
如果想令某容器中的每個對象都執行某項任務,並且等待所有任務執行完畢,那麼就可以使用這個GCD特性來實現:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 創建dispatch group dispatch_group_t group = dispatch_group_create(); for (id object in colletion){ // 派發任務 dispatch_group_async(group, queue, ^{ [object performTask]; }); } // 等待組內任務執行完畢 dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
假如當前線程不應阻塞,而開發者又想在這組任務全部完成後收到消息,可用notify函數來替代wait
// notify回調時所選用的隊列可以根據情況來定,這裡用的是主隊列 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 完成任務後繼續接下來的操作 });
也可以把某些任務放在優先級高的線程上執行,同時所有任務仍然屬於一個group
// 創建兩個優先級不同的隊列 dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); dispatch_queue_t HighPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); // 創建dispatch group dispatch_group_t group = dispatch_group_create(); for (id object in lowPriorityColletion){ dispatch_group_async(group, lowPriorityQueue, ^{ [object performTask]; }); } for (id object in HighPriorityColletion){ dispatch_group_async(group, HighPriorityQueue, ^{ [object performTask]; }); } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 完成任務後繼續接下來的操作 });
而dispatch group也不是必須使用,編譯某個容器,並在其每個元素上執行任務,可以用apply函數,下面以數組為例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply(array.count, queue, ^(size_t i) { id object = array[i]; [object performTask]; });
但是使用apply函數會持續阻塞,直到所有任務都執行完畢為止。由此可見,假如把塊派給了當前隊列(或者體系中高於當前隊列的串行隊列),就將導致死鎖。若想在後台執行任務,則應使用dispatch group。
單例模式的常見實現方式為,在類中編寫名為sharedInstance的方法,該方法只會返回全類共用的單例實例,而不會每次調用時都創建新的實例。下面是用同步塊來實現單例模式:
@implementation EOCClass + (id)sharedInstance{ static EOCClass *sharedInstance = nil; @synchronized(self){ if(!sharedInstance){ sharedInstance = [[self alloc] init]; } } return sharedInstance; } @end
GCD引入了一項特性,使單例實現起來更為容易
@implementation EOCClass + (id)sharedInstance{ static EOCClass *sharedInstance = nil; // 每次調用都必須使用完全相同的標志,需要將標志聲明為static static dispatch_once_t onceToken; // 只會進行一次 dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } @end
dispatch_once可以簡化代碼並徹底保證線程安全,所有問題都由GCD在底層處理。而且dispatch_once更高效,它沒有使用重量級的同步機制。
使用GCD時,經常需要判斷當前代碼正在哪個隊列上執行。dispatch_get_current_queue函數返回的就是當前正在執行代碼的隊列。不過iOS與Mac OS X都已經將它廢除了,要避免使用它。
同步隊列操作有可能會發生死鎖:
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NUll); - (NSString*)someString{ __block NSString *localSomeString; dispatch_sync(_syncQueue, ^{ localSomeString = _someString; }); return localSomeString; } - (void)setSomeString:(NSString*)someString{ dispatch_sync(_syncQueue, ^{ _someString = someString; }); }
假如調用getter方法的隊列恰好是同步操作所針對的隊列(_syncQueue),那麼dispatch_sync就會一直不返回,直到塊執行完畢。可是應該執行塊的那個目標隊列卻是當前隊列,它又一直阻塞,等待目標隊列將塊執行完畢。這樣一來就出現死鎖。
這時候或許可以用dispatch_get_current_queue來解決(在這種情況下,更好的做法是確保同步操作所用的隊列絕不會訪問屬性)
- (NSString*)someString{ __block NSString *localSomeString; dispatch_block_t block = ^{ localSomeString = _someString; }; // 執行塊的隊列如果是_syncQueue,則不派發直接執行塊 if (dispatch_get_current_queue() == _syncQueue) { block(); } else{ dispatch_sync(_syncQueue, block); } return localSomeString; }
但是這種做法只能處理一些簡單的情況,如果遇到下面情況,仍有死鎖風險:
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL); dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL); dispatch_sync(queueA, ^{ dispatch_sync(queueB, ^{ dispatch_sync(queueA, ^{ // 死鎖 }); }); });
因為這個操作是針對queueA的,所以必須等最外層的dispatch_sync執行完畢才行,而最外層的dispatch_sync又不可能執行完畢,因為它要等到最內層的dispatch_sync執行完畢,於是就死鎖了。
如果嘗試用dispatch_get_current_queue來解決:
dispatch_sync(queueA, ^{ dispatch_sync(queueB, ^{ dispatch_block_t block = ^{ /* ... */ }; if (dispatch_get_current_queue() == queueA) { block(); } else{ dispatch_sync(queueA, block); } }); });
這樣做仍然會死鎖,因為dispatch_get_current_queue()返回的是當前隊列,即queueB,這樣的話仍然會執行針對queueA的同步派發操作,於是同樣會死鎖。
由於派發隊列是按層級來組織的,這意味著排在某條隊列中的塊會在其上級隊列裡執行。隊列間的層級關系會導致檢查當前隊列是否為執行同步派發所用的隊列這種方法並不總是奏效。
要解決這個問題,最好的辦法是通過GCD提供的功能來設定隊列特有數據。此功能可以把任意數據以鍵值對的形式關聯到隊列裡。最重要的是,假如獲取不到關聯數據,那麼系統會沿著層級體系向上查找,直至找到數據或到達根隊列為止。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL); dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL); static int kQueueSpecific; CFStringRef queueSpecificValue = CFSTR("queueA"); // 在queueA上設置隊列特定值 dispatch_queue_set_specific(queueA, &kQueueSpecific, (void*)queueSpecificValue, (dispatch_function_t)CFRelease); dispatch_sync(queueB, ^{ dispatch_block_t block = ^{ NSLog(@"No deadlock!"); }; CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); if (retrievedValue) { block(); } else{ dispatch_sync(queueA, block); } });
dispatch_queue_set_specific函數首個參數為待設置隊列,其後兩個參數是鍵和值(函數按指針值來比較鍵,而不是其內容)。最後一個參數是析構函數。dispatch_function_t類型定義如下:
typedef void (*dispatch_function_t)(void*)
本例傳入CFRelease做參數,用以清理舊值。
隊列特定數據所提供的這套簡單易用的機制,避免了使用dispatch_get_current_queue時經常遇到的陷阱。但是,可以在調試程序時使用dispatch_get_current_queue這個已經廢棄的方法,只是別把它編譯到發行版程序裡就行。