41.多用派發(fā)隊列,少用同步鎖
在Objective-C中陷猫,如果有多個線程要執(zhí)行同一份代碼秫舌,那么有時可能會出問題的妖。這種情況下,通常要使用鎖來實現(xiàn)某種同步機制足陨。在GCD出現(xiàn)之前嫂粟,有兩種辦法,第一種是采用內(nèi)置的“同步塊”(synchronization block):
//第一種方式:@synchronized
-(void)synchronizedMethod{
@synchronized (self) {
//Safe
}
}
這種寫法會根據(jù)給定的對象墨缘,自動創(chuàng)建一個鎖星虹,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處镊讼,鎖就釋放了宽涌。在本例中,同步行為所針對的對象是self蝶棋。這么寫通常沒錯卸亮,因為它可以保證每個對象實例都能不受干擾地運行其synchronizedMethod方法。然而玩裙,濫用@synchronized(self)則會降低代碼效率兼贸,因為共用同一個鎖的那些同步塊,都必須按順序執(zhí)行吃溅。若是在self對象上頻繁加鎖溶诞,那么程序可能要等另一段與此無關的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當前代碼决侈,這樣做其實并沒有必要螺垢。
另一個辦法是直接使用NSLock對象:
//第二種方式:NSLock
-(void)synchronizedMethod{
[_lock lock];
//Safe
[_lock unlock];
}
也可以使用NSRecursiveLock這種“遞歸鎖”,線程能夠多次持有該鎖赖歌,而不會出現(xiàn)死鎖現(xiàn)象甩苛。
這兩種方法都很好,不過也有其缺陷俏站。比方說,在極端情況下痊土,同步塊會導致死鎖肄扎,另外,其效率也不見得很高赁酝,而如果直接使用鎖對象的話犯祠,一旦遇到死鎖,就會非常麻煩酌呆。
替代方案就是使用GCD衡载,它能以更簡單、更高效的形式為代碼加鎖隙袁。比方說痰娱,屬性就是開發(fā)者經(jīng)常需要同步的地方弃榨,這種屬性需要做成“原子的”。用atomic特質(zhì)來修飾屬性梨睁,即可實現(xiàn)這一點(第6條)鲸睛。而開發(fā)者如果想自己來編寫訪問方法的話,那么通常會這樣寫:
-(NSString *)someString{
@synchronized (self) {
return _someString;
}
}
-(void)setSomeString:(NSString *)someString{
@synchronized (self) {
_someString = someString;
}
}
濫用@syncronized(self)會很危險坡贺,因為所有同步塊都會彼此搶奪同一個鎖官辈。要是有很多屬性都這么寫的話,那么每個屬性的同步塊都要等其他所有同步塊執(zhí)行完畢才能執(zhí)行遍坟,這也許并不是開發(fā)者想要的效果拳亿。我們只是想令每個屬性各自獨立地同步。
這么做雖然能提供某種程度的“線程安全”愿伴,但卻無法保證訪問該對象時絕對是線程安全的肺魁。當然,訪問屬性的操作確實是“原子的”公般。使用屬性時万搔,必定能從中獲取到有效值,然而在同一個線程上多次調(diào)用獲取方法官帘,每次獲取到的結(jié)果卻未必相同瞬雹。在兩次訪問操作之間,其他線程可能會寫入新的屬性值刽虹。
有種簡單而高效的辦法可以代替同步塊或鎖對象酗捌,那就是使用“串行同步隊列”(serial synchronization queue)。將讀取操作及寫入操作都安排在同一個隊列里涌哲,即可保證數(shù)據(jù)同步胖缤。其用法如下:
_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;
});
}
此模式的思路是:把設置操作與獲取操作都安排在序列化的隊列里執(zhí)行,這樣的話阀圾,所有針對屬性的訪問操作就都同步了哪廓。為了使塊代碼能夠設置局部變量,獲取方法中用到了__block語法初烘。全部加鎖任務都在GCD中處理涡真,而GCD是在相當深的底層來實現(xiàn)的,于是能夠做許多優(yōu)化肾筐。
還可以進一步優(yōu)化哆料,設置方法并不一定非得是同步的。設置實例變量所用的塊吗铐,并不需要向設置方法返回什么值东亦。也就是說,設置方法的代碼可以改成這樣:
-(void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
這次只是把同步派發(fā)改成了異步派發(fā)唬渗,從調(diào)用者的角度來看典阵,這個小改動可以提升設置方法的執(zhí)行速度奋渔,而讀取操作與寫入操作依然會按順序執(zhí)行。但這么改有個壞處:如果你測一下程序性能萄喳,那么可能會發(fā)現(xiàn)這種寫法比原來慢卒稳,因為執(zhí)行異步派發(fā)時,需要拷貝塊他巨。若拷貝塊所用的時間明顯超過執(zhí)行塊所花的時間充坑,則這種做法比原來慢。由于這里所舉的例子很簡單染突,所以改完之后很可能會變慢捻爷。然而,若是派發(fā)給隊列的塊要執(zhí)行更為繁重的任務份企,那么仍然可以考慮這種備選方案也榄。
多個獲取方法可以并發(fā)執(zhí)行,而獲取方法與設置方法之間不能并發(fā)執(zhí)行司志,利用這個特點甜紫,還能寫出更快一些的代碼來。此時正可以體現(xiàn)出GCD寫法的好處骂远。用同步塊或鎖對象囚霸,是無法輕易實現(xiàn)出下面這種方案的。這次不用串行隊列激才,而改用并發(fā)隊列(concurrent queue):
_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{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
像現(xiàn)在這樣寫代碼拓型,還無法正確實現(xiàn)同步。所有讀取操作與寫入操作都會在同一個隊列上執(zhí)行瘸恼,不過由于是并發(fā)隊列劣挫,所以讀取與寫入操作可以隨時執(zhí)行。而我們恰恰不想讓這些操作隨意執(zhí)行东帅。此問題用一個簡單的GCD功能即可解決压固。它就是柵欄(barrier)。下列函數(shù)可以向隊列中派發(fā)塊靠闭,將其作為柵欄使用:
void dispatch_barrier_async(dispatch_queue_t queue,
dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue,
dispatch_block_t block);
在隊列中邓夕,柵欄塊必須單獨執(zhí)行,不能與其他塊并行阎毅。這只對并發(fā)隊列有意義,因為串行隊列中的塊總是按順序逐個來執(zhí)行的点弯。并發(fā)隊列如果發(fā)現(xiàn)接下來要處理的塊是個柵欄塊(barrier block),那么就一直要等當前所有并發(fā)塊都執(zhí)行完畢扇调,才會單獨執(zhí)行這個柵欄。待柵欄執(zhí)行過后抢肛,再按正常方式繼續(xù)向下處理狼钮。
在本例中碳柱,可以用柵欄塊來實現(xiàn)屬性的設置方法。在設置方法中使用了柵欄塊之后熬芜,對屬性的讀取操作依然可以并發(fā)執(zhí)行莲镣,但是寫入操作卻必須單獨執(zhí)行了。下圖演示的這個隊列中涎拉,有許多讀取操作瑞侮,而且還有一個寫入操作:
實現(xiàn)代碼很簡單:
_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{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
這種做法肯定比使用串行隊列要快。注意鼓拧,設置函數(shù)也可以改用同步的柵欄塊來實現(xiàn)半火,那樣做可能會更高效。最好還是測一測每種做法的性能季俩,然后從中選出最適合當前場景的方案钮糖。
要點:
- 派發(fā)隊列可用來表述同步語義,這種做法要比使用@synchronized塊或NSLock對象更簡單酌住。
- 將同步與異步派發(fā)結(jié)合起來店归,可以實現(xiàn)與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執(zhí)行異步派發(fā)的線程酪我。
- 使用同步隊列及柵欄塊消痛,可以令同步行為更加高效。
42.多用GCD祭示,少用preformSelector系列方法
Objective-C本質(zhì)上是一門非常動態(tài)的語言肄满,NSObject定義了幾個方法,令開發(fā)者可以隨意調(diào)用任何方法质涛。這幾個方法可以推遲執(zhí)行方法調(diào)用稠歉,也可以指定運行方法所用的線程。這些功能原來很有用汇陆,但是在出現(xiàn)了大中樞派發(fā)及塊這樣的新技術(shù)之后怒炸,就顯得不那么必要了。雖然有些代碼還是會經(jīng)常用到它們毡代,但是盡量避開為好阅羹。
這其中最簡單的是“performSelector:”。該方法的簽名如下教寂,它接受一個參數(shù)捏鱼,就是要執(zhí)行的那個選擇子:
- (id)performSelector:(SEL)aSelector;
該方法與直接調(diào)用選擇子等效。所以下面兩行代碼的執(zhí)行效果相同:
[object performSelector:@selector(selectorName)];
[object selectorName];
這種方式看上去似乎多余酪耕。如果某個方法只是這么來調(diào)用的話导梆,那么此方式確實多余。然而,如果選擇子是在運行期決定的看尼,那么就能體現(xiàn)出此方法的強大之處了递鹉。這就等于在動態(tài)綁定之上再次使用動態(tài)綁定,因而可以實現(xiàn)出下面這種功能:
SEL selecotr;
if(/*some condition*/){
selecotr = @selector(foo);
}else if (/*some other condition*/){
selecotr = @selector(bar);
}else{
selecotr = @selector(baz);
}
[object performSelector:selecotr];
這種編程方式極為靈活藏斩,經(jīng)初锝幔可用來簡化復雜的代碼。還有一種用法狰域,就是先把選擇子保存起來媳拴,等某個事件發(fā)生之后再調(diào)用。不管哪種用法北专,編譯器都不知道要執(zhí)行的選擇子是什么禀挫,這必須到了運行期才能確定。然而拓颓,使用此特性的代價是语婴,如果在ARC下編譯代碼,那么編譯器就會發(fā)出如下警告信息:
warning:performSelector may cause a leak because its selector
is unknown [-Warc-performSelector-leaks]
原因在于:編譯器并不知道將要調(diào)用的選擇子是什么驶睦,因此砰左,也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚场航。而且缠导,由于編譯器不知道方法名,所以就沒辦法運用ARC的內(nèi)存管理規(guī)則來判定返回值是不是應該釋放溉痢。鑒于此僻造,ARC采用了比較謹慎的做法,就是不添加釋放操作孩饼。然而這么做可能導致內(nèi)存泄露髓削,因為方法在返回對象時可能已經(jīng)將其保留了。
考慮下面這段代碼:
SEL selecotr;
if(/*some condition*/){
selecotr = @selector(newObject);
}else if (/*some other condition*/){
selecotr = @selector(copy);
}else{
selecotr = @selector(someProperty);
}
id ret = [object performSelector:selecotr];
如果調(diào)用的是前兩個選擇子之一镀娶,那么ret對象應由這段代碼來釋放立膛,而如果是第三個選擇子,則無須釋放梯码。不僅在ARC環(huán)境下應該如此宝泵,而在在非ARC環(huán)境下也應該這么做,這樣才算嚴格遵循了方法的命名規(guī)范轩娶。如果不使用ARC(此時編譯器就不發(fā)警告信息了)儿奶,那么在前兩種情況下需要手動釋放ret對象,而在后一種情況下則不需要釋放鳄抒。這個問題很容易忽視廓握,而且就算用靜態(tài)分析器搅窿,也很難偵測到隨后的內(nèi)存泄露。performSelector系列的方法之所以要謹慎使用隙券,這就是其中一個原因。
這些方法不甚理想闹司,另一個原因在于:返回值只能是void或?qū)ο箢愋陀樽小1M管所要執(zhí)行的選擇子也可以返回void,但是performSelector方法的返回值類型畢竟是id游桩。如果想返回整數(shù)或浮點等類型的值牲迫,那么就需要執(zhí)行一些復雜的轉(zhuǎn)換操作了,而這種轉(zhuǎn)換很容易出錯借卧。由于id類型表示指向任意Objective-C對象的指針盹憎,所以從技術(shù)上來講,只要返回值的大小和指針所占大小相同就行铐刘,也就是說:在32位架構(gòu)的計算機上陪每,可以返回任意32位大小的類型;而在64位架構(gòu)的計算機上镰吵,則可返回任意64位大小的類型檩禾。若返回值的類型為C語言結(jié)構(gòu)退,則不可使用performSelector方法疤祭。
performSelector還有如下幾個版本盼产,可以在發(fā)消息時順便傳遞參數(shù):
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
比方說,可以用下面這個版本來設置對象中名為value的屬性值:
id object = /* an object with a property called value */;
id newValue = /* new value for the property */;
[object performSelector:@selector(setValue:) withObject:newValue];
這些方法貌似有用勺馆,但其實局限頗多戏售。由于參數(shù)類型是id,所以傳入的參數(shù)必須是對象才行草穆。如果選擇子所接受的參數(shù)是整數(shù)或浮點數(shù)灌灾,那就不能采用這些方法了。此外续挟,選擇子最多只能接受兩個參數(shù)紧卒,也就是調(diào)用“performSelector: withObject: withObject:”這個版本。而在參數(shù)不止兩個的情況下诗祸,則沒有對應的performSelector方法能夠執(zhí)行此種選擇子跑芳。
performSelector系列方法haunted有個功能,就是可以延后執(zhí)行選擇子直颅,或?qū)⑵浞旁诹硪粋€線程上執(zhí)行博个。下面列出了此方法中一些更為常用的版本:
- (void)performSelector:(SEL)aSelector
withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr
withObject:(nullable id)arg
waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector
withObject:(nullable id)arg
waitUntilDone:(BOOL)wait;
然而很快就會發(fā)覺,這些方法太過局限了功偿。例如盆佣,具備延后執(zhí)行功能的那些方法都無法處理帶有兩個參數(shù)的選擇子往堡。而能夠指定執(zhí)行線程的那些方法,則與之類似共耍,所以也不是特別通用虑灰。如果要用這些方法,就得把許多參數(shù)都打包到字典中痹兜,然后在受調(diào)用的方法里將其提取出來穆咐,這樣會增加開銷,而且還可能出bug字旭。
如果改用其他替代方案对湃,那就不受這些限制了。最主要的替代方案就是使用塊遗淳。而且拍柒,performSelector系列方法所提供的線程功能,都可以通過在大中樞派發(fā)機制中使用塊來實現(xiàn)屈暗。延后執(zhí)行可以用dispatch_after來實現(xiàn)拆讯,在另一個線程上執(zhí)行任務則可以通過dispatch_sync及dispatch_async來實現(xiàn)。
例如恐锦,要延后執(zhí)行某項任務往果,可以有下面兩種實現(xiàn)方式,而我們應該優(yōu)先考慮第二種:
//Using performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0*NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
[self doSomething];
});
想把任務放在主線程上執(zhí)行一铅,也可以有下面兩種方式陕贮,而我們還是應該優(yōu)選后者:
//Using performSelectorOnMainThread: withObject: waitUntilDone:
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
//Using dispatch_async
//(or if waitUntilDone is YES, then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
waitUntilDone為NO時相當于使用dispatch_async;waitUntilDone為YES時相當于使用dispatch_sync潘飘。
要點:
- performSelector系列方法在內(nèi)存管理方面容易有疏失肮之。它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無法插入適當?shù)膬?nèi)存管理方法卜录。
- performSelector系列方法所能處理的選擇子太過局限了戈擒,選擇子的返回值類型及發(fā)送給方法的參數(shù)個數(shù)都受到限制。
- 如果想把任務放在另一個線程上執(zhí)行艰毒,那么最好不要用performSelector系列方法筐高,而是應該把任務封裝到塊里,然后調(diào)用大中樞派發(fā)機制的相關方法來實現(xiàn)丑瞧。
43.掌握GCD及操作隊列的使用時機
GCD技術(shù)確實很棒柑土,不過有時候采用標準系統(tǒng)庫的組件,效果會更好绊汹。一定要了解每項技巧的使用時機稽屏,如果選錯了工具,那么編出來的代碼就會難于維護西乖。
很少有其他技術(shù)能與GCD的同步機制想媲美狐榔。對于那些只需執(zhí)行一次的代碼來說坛增,也是如此,使用GCD的dispatch_once最為方便薄腻。然而收捣,在執(zhí)行后臺任務時接谨,GCD并不一定是最佳方式稀火。還有一種技術(shù)叫做NSOperationQueue,它雖然與GCD不同脸哀,但是卻與之相關嫁乘,開發(fā)者可以把操作以NSOperation子類的形式放在隊列中,而這些操作也能夠并發(fā)執(zhí)行球碉。其與GCD派發(fā)隊列有相似之處蜓斧,這并非巧合≌龆“操作隊列”(operation queue)在GCD之前就有了挎春,其中某些設計原理操作隊列而流行,GCD就是基于這些原理構(gòu)建的豆拨。實際上直奋,從iOS4與Mac OS X 10.6開始,操作隊列在底層是用GCD來實現(xiàn)的施禾。
在兩者的諸多差別中脚线,首先要注意:GCD是純C的API,而操作隊列則是Objective-C的對象弥搞。在GCD中邮绿,任務用塊來表示,而塊是個輕量級數(shù)據(jù)結(jié)構(gòu)攀例。與之相反船逮,“操作”(operation)則是個更為重量級的Objective-C對象。雖說如此粤铭,但GCD并不總是最佳方案挖胃。有時候采用對象所帶來的開銷微乎其微,使用完整對象所帶來的好處反而大大超過其缺點梆惯。
使用NSOperationQueue類的“addOperationWithBlock:”方法搭配NSBlockOperation類來使用操作隊列酱鸭,其語法與純GCD方式非常相似。使用NSOperation及NSOperationQueue的好處如下:
- 取消某個操作加袋。如果使用操作隊列凛辣,那么想要取消操作是很容易的。運行任務之前职烧,可以在NSOperation對象上調(diào)用cancel方法扁誓,該方法會設置對象內(nèi)的標志位防泵,用以表明此任務不需執(zhí)行,不過蝗敢,已經(jīng)啟動的任務無法取消捷泞。若是不是使用操作隊列,而是把塊安排到GCD隊列寿谴,那就無法取消了锁右。那套架構(gòu)是“安排好任務之后就不管了”(fire and forget)。開發(fā)者可以在應用程序?qū)幼约簛韺崿F(xiàn)取消功能讶泰,不過這樣做需要編寫很多代碼咏瑟,而那些代碼其實已經(jīng)由操作隊列實現(xiàn)好了。
- 指定操作間的依賴關系痪署。一個操作可以依賴其他多個操作码泞。開發(fā)者能夠指定操作之間的依賴體系,使特定的操作必須在另外一個操作順利執(zhí)行完畢后方可執(zhí)行狼犯。比方說余寥,從服務器端下載并處理文件的動作,可以用操作來表示悯森,而在處理其他文件之前宋舷,必須先下載“清單文件”(manifest file)。后續(xù)的下載操作瓢姻,都要依賴于先下載清單文件這一操作祝蝠。如果操作隊列允許并發(fā)的話,那么后續(xù)的多個下載操作就可以同時執(zhí)行汹来,但前提是它們所依賴的那個清單文件下載操作已經(jīng)執(zhí)行完畢续膳。
- 通過鍵值觀測機制監(jiān)控NSOperation對象的屬性。NSOperation對象有許多屬性都適合通過鍵值觀測機制(KVO)來監(jiān)聽收班,比如可以通過isCancelled屬性來判斷任務是否已取消坟岔,又比如可以通過isFinished屬性來判斷任務是否已完成。如果想在某個任務變更其狀態(tài)時得到通知摔桦,或是想用比GCD更為精細的方式來控制所要執(zhí)行的任務社付,那么鍵值觀測機制會很有用。
- 指定操作的優(yōu)先級邻耕。操作的優(yōu)先級表示此操作與隊列中的其他操作之間的優(yōu)先級關系鸥咖。優(yōu)先級高的操作先執(zhí)行,優(yōu)先級低的后執(zhí)行兄世。操作隊列的調(diào)度算法雖“不透明”啼辣,但必然是經(jīng)過一番深思熟慮才寫成的。反之御滩,GCD則沒有直接實現(xiàn)此功能的辦法鸥拧。GCD的隊列確實有優(yōu)先級党远,不過那是針對整個隊列來說的,而不是針對每個塊來說的富弦。而令開發(fā)者在GCD之上自己來編寫調(diào)度算法沟娱,又不太合適。因此腕柜,在優(yōu)先級這一點上济似,操作隊列所提供的功能要比GCD更為便利。NSOperation對象也有“線程優(yōu)先級”(thread priority)盏缤,這決定了運行此操作的線程處在何種優(yōu)先級上砰蠢。用GCD也可以實現(xiàn)此功能,然而采用操作隊列更簡單唉铜,只需設置一個屬性娩脾。
- 重用NSOperation對象。系統(tǒng)內(nèi)置了一些NSOperation的子類(比附NSBlockOperation)供開發(fā)者調(diào)用打毛,要是不想用這些固有子類的話,那就得自己來創(chuàng)建了俩功。這些類就是普通的Objective-C對象幻枉,能夠存放任何信息。對象在執(zhí)行時可以充分利用存放于其中的信息诡蜓,而且還可以隨意調(diào)用定義在類中的方法熬甫。這就比派發(fā)隊列中那些簡單的塊要強大許多。這些NSOperation類可以在代碼中多次使用蔓罚,它們符合軟件開發(fā)中的“不重復”(Don’t Repeat Yourself,DRY)原則椿肩。
操作隊列有很多地方勝過派發(fā)隊列。操作隊列提供了多種執(zhí)行任務的方式豺谈,而且都是寫好了的郑象,直接就能使用。開發(fā)者不用再編寫復雜的調(diào)度器茬末,也不用自己來實現(xiàn)取消操作或者指定操作優(yōu)先級的功能厂榛,這些事情操作隊列都已經(jīng)實現(xiàn)好了。
有一個API選用了操作隊列而非派發(fā)隊列丽惭,這就是NSNotificationCenter击奶,開發(fā)者可通過其中的方法來注冊監(jiān)聽器,以便在發(fā)生相關事件時得到通知责掏,而這個方法接受的參數(shù)是塊柜砾,不是選擇子。方法原型如下:
- (id <NSObject>)addObserverForName:(nullable NSString *)name
object:(nullable id)obj
queue:(nullable NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block ;
某些功能確實可以用高層的Objective-C方法來做换衬,但這并不等于說它就一定比底層實現(xiàn)方案好痰驱。要想確定哪種方案更佳证芭,最好還是測試一下性能。
要點:
- 在解決多線程與任務管理問題時萄唇,派發(fā)隊列并非唯一方案檩帐。
- 操作隊列提供了一套高層的Objective-C API,能實現(xiàn)純GCD所具備的絕大部分功能另萤,而且還能完成一些更為復雜的操作湃密,那些操作若改用GCD來實現(xiàn),則需另外編寫代碼四敞。
44.通過Dispatch Group機制泛源,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務
dispatch group(派發(fā)分組,調(diào)度組)是GCD的一項特性忿危,能夠把任務分組达箍。調(diào)用者可以等待這組任務執(zhí)行完畢,也可以在提供回調(diào)函數(shù)之后繼續(xù)往下執(zhí)行铺厨,這組任務完成時缎玫,調(diào)用者會得到通知。這個功能有許多用途解滓,其中最重要赃磨、最值得注意的用法,就是把將要并發(fā)執(zhí)行的多個任務合為一個組洼裤,于是調(diào)用者就可以知道這些任務何時才能全部執(zhí)行完畢邻辉。比方說,可以把壓縮一系列文件的任務表示成dispatch group腮鞍。
下面這個函數(shù)可以創(chuàng)建dispatch group:
dispatch_group_t
dispatch_group_create(void);
dispatch group就是一個簡單的數(shù)據(jù)結(jié)構(gòu)值骇,這種數(shù)據(jù)結(jié)構(gòu)彼此之間沒什么區(qū)別,它不像派發(fā)隊列移国,后者還有個用來區(qū)別身份的標識符吱瘩。想把任務編組,有兩種辦法迹缀。第一種是用下面這個函數(shù):
void
dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
它是普通dispatch_async函數(shù)的變體搅裙,比原來多一個參數(shù),用于表示待執(zhí)行的塊所屬的組裹芝。還有種辦法能夠指定任務所屬的dispatch group部逮,那就是使用下面這一對函數(shù):
void
dispatch_group_enter(dispatch_group_t group);
void
dispatch_group_leave(dispatch_group_t group);
前者能夠使分組里正要執(zhí)行的任務數(shù)遞增,而后者則使之遞減嫂易。由此可知兄朋,調(diào)用了dispatch_group_enter以后,必須有與之對應的dispatch_group_leave才行。這與引用計數(shù)相似颅和,要使用引用計數(shù)傅事,就必須令保留操作與釋放操作彼此對應,以防內(nèi)存泄露峡扩。而在使用dispatch_group時蹭越,如果調(diào)用enter之后,沒有相應的leave操作教届,那么這一組任務就永遠執(zhí)行不完响鹃。
下面這個函數(shù)可用于等待dispatch group執(zhí)行完畢:
long
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
此函數(shù)接受兩個參數(shù),一個是要等待的group案训,另一個是代表等待時間的timeout值买置。timeout參數(shù)表示函數(shù)在等待dispatch group執(zhí)行完畢時,應該阻塞多久强霎。如果執(zhí)行dispatch group所需的時間小于timeout忿项,則返回0,否則返回非0值城舞。此函數(shù)也可以取常量DISPATCH_TIME_FOREVER轩触,這表示函數(shù)會一直等著dispatch group執(zhí)行完,而不會超時家夺。
除了可以用上面那個函數(shù)等待dispatch group執(zhí)行完畢之外怕膛,也可以換個辦法,使用下列函數(shù):
void
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
與wait函數(shù)略有不同的是:開發(fā)者可以向此函數(shù)傳入塊秦踪,等dispatch group執(zhí)行完畢之后,塊會在特定的線程上執(zhí)行掸茅。假如當前線程不應阻塞椅邓,而開發(fā)者又想在那些任務全部完成時得到通知,那么此做法就很有必要了昧狮。比方說景馁,在Mac OS X與iOS系統(tǒng)中,都不應阻塞主線程逗鸣,因為所有UI繪制及事件處理都要在主線程上執(zhí)行合住。
如果想令數(shù)組中的每個對象都執(zhí)行某項任務,并且想等待所有任務執(zhí)行完畢撒璧,那么就可以使用這個GCD特性來實現(xiàn)透葛。代碼如下:
dispatch_queue_t queue =
dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for(id object in collection){
dispatch_group_async(dispatchGroup,
queue, ^{
[object performTask];
});
}
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
//Continue processing after completing tasks
若當前線程不應阻塞,則可以用notify函數(shù)來取代wait:
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,
notifyQueue, ^{
//Continue processing after completing tasks
});
notify回調(diào)時所選用的隊列卿樱,完全應該根據(jù)具體情況來定僚害。這里使用了主隊列,這是種常見寫法繁调,也可以用自定義的串行隊列或全局并發(fā)隊列萨蚕。
本例中靶草,所有任務都派發(fā)到同一個隊列之中。但實際上未必一定要這樣做岳遥。也可以把某些任務放在優(yōu)先級高的線程上執(zhí)行奕翔,同時仍然把所有任務都歸入同一個dispatch group,并在執(zhí)行完畢時獲得通知:
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_t dispatchGroup = dispatch_group_create();
for(id object in lowPriorityObjects){
dispatch_group_async(dispatchGroup,
lowPriorityQueue,
^{
[object performTask];
});
}
for(id object in highPriorityObjects){
dispatch_group_async(dispatchGroup,
highPriorityQueue,
^{
[object performTask];
});
}
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,
notifyQueue,
^{
//Countinue processing after completing tasks
});
除了像上面這樣把任務提交到并發(fā)隊列之外浩蓉,也可以把任務提交至各個串行隊列中派继,并用dispatch group跟蹤其執(zhí)行狀況。然而妻往,如果所有任務都排在同一個串行隊列里面互艾,那么dispatch group就用處不大了。因為此時任務總要逐個執(zhí)行讯泣,所以只需在提交完全部任務之后再提交一個塊即可纫普,這樣做與通過notify函數(shù)等待dispatch group執(zhí)行完畢然后再回調(diào)塊是等效的:
dispatch_queue_t queue =
dispatch_queue_create("com.effectiveobjectivec.queue", NULL);
for(id object in collections){
dispatch_async(queue, ^{
[object performTask];
});
}
dispatch_async(queue, ^{
//Continue processing after completing tasks
});
上面這段代碼表明,開發(fā)者未必總是需要使用dispatch group好渠。有時候采用單個隊列搭配標準的異步派發(fā)昨稼,也可以實現(xiàn)同樣效果。
為了執(zhí)行隊列中的塊拳锚,GCD會在適當?shù)臅r機自動創(chuàng)建新線程或復用舊線程假栓。如果使用并發(fā)隊列,那么其中有可能會有多個線程霍掺,這也就意味著多個塊可以并發(fā)執(zhí)行匾荆。在并發(fā)隊列中,執(zhí)行任務所用的并發(fā)線程數(shù)量杆烁,取決于各種因素牙丽,而GCD主要是根據(jù)系統(tǒng)資源狀況來判斷這些因素的。加入CPU有多個核心兔魂,并發(fā)隊列中有大批任務等待執(zhí)行烤芦,那么GCD就可能會給該隊列配置多個線程。通過dispatch group所提供的這種簡便方式析校,既可以并發(fā)執(zhí)行一系列給定的任務构罗,又能在全部任務結(jié)束時得到通知。由于GCD有并發(fā)隊列機制智玻,所以能夠根據(jù)可用的系統(tǒng)資源狀況來并發(fā)執(zhí)行任務遂唧。而開發(fā)者則可用專注于業(yè)務邏輯代碼,無須再為了處理并發(fā)任務而編寫復雜的調(diào)度器吊奢。
在前面的例子中蠢箩,我們遍歷某個collection,并在其每個元素上執(zhí)行任務,而這也可以用另外一個GCD函數(shù)來實現(xiàn):
void
dispatch_apply(size_t iterations, dispatch_queue_t queue,
void (^block)(size_t));
此函數(shù)會將塊反復執(zhí)行一定的次數(shù)谬泌,每次傳給塊的參數(shù)值都會遞增滔韵,從0開始,直至”iterations-1“掌实。其用法如下:
dispatch_queue_t queue =
dispatch_queue_create("com.effectiveobjectivec.queue", NULL);
dispatch_apply(10, queue, ^(size_t i) {
//Perform task
});
采用簡單的for循環(huán)陪蜻,從0遞增至9,也能實現(xiàn)同樣的效果:
for(int i=0;i<10;i++){
//Perform task
}
注意:dispatch_apply所用的隊列可以是并發(fā)隊列贱鼻。如果采用并發(fā)隊列宴卖,那么系統(tǒng)就可以根據(jù)資源狀況來并行執(zhí)行這些塊了,這與使用dispatch group的那段代碼一樣邻悬。上面這個for循環(huán)要處理的collection若是數(shù)組症昏,則可以用dispatch_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];
});
這個例子再次表明:未必總要使用dispatch_group。然而父丰,dispatch_apply會持續(xù)阻塞肝谭,直到所有任務都執(zhí)行完畢未知。由此可見:假如把塊派發(fā)給了當前隊列(或者體系中高于當前隊列的某個串行隊列)蛾扇,將導致死鎖攘烛。若想在后臺執(zhí)行任務,則應使用dispatch group镀首。
** 要點:**
- 一系列任務可歸入一個dispatch group之中坟漱。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知。
- 通過dispatch group更哄,可以在并發(fā)式派發(fā)隊列里同時執(zhí)行多項任務芋齿。此時GCD會根據(jù)系統(tǒng)資源狀況來調(diào)度這些并發(fā)執(zhí)行的任務。
45.使用dispatch_once來執(zhí)行只需執(zhí)行一次的線程安全代碼
單例模式(singleton)對Objective-C開發(fā)者來說并不陌生成翩,常見的實現(xiàn)方式為:在類中編寫名為sharedInstance的方法觅捆,該方法只會返回全類共用的單例實例,而不會在每次調(diào)用時都創(chuàng)建新的實例捕传。假設有個類叫EOCClass,那么這個共享實例的方法一般都會這樣寫:
+(instancetype)sharedInstance{
static EOCClass *sharedInstance = nil;
@synchronized (self) {
if(!sharedInstance){
sharedInstance = [[self alloc]init];
}
}
return sharedInstance;
}
為保證線程安全扩劝,上述代碼將創(chuàng)建單例實例的代碼包裹在同步塊里庸论。
不過,GCD引入了一項特性棒呛,能使單例實現(xiàn)起來更為容易聂示。所用的函數(shù)是:
void
dispatch_once(dispatch_once_t *predicate,
dispatch_block_t block);
此函數(shù)接受類型為dispatch_once_t的特殊參數(shù),作者稱其為“標記”(token)簇秒,此外還接受塊參數(shù)鱼喉。對于給定的標記來說,該函數(shù)保證相關的塊必定會執(zhí)行,其僅執(zhí)行一次扛禽。首次調(diào)用該函數(shù)時锋边,必然會執(zhí)行塊中的代碼,最重要的一點在于编曼,此操作完全是線程安全的豆巨。請注意,對于只需執(zhí)行一次的塊來說掐场,每次調(diào)用函數(shù)時傳入的標記都必須完全相同往扔。因此,開發(fā)者通常將標記變量聲明在static或global作用域里熊户。
剛才實現(xiàn)單例模式所用的sharedInstance方法萍膛,可以用此函數(shù)來改寫:
+(instancetype)sharedInstance{
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc]init];
});
return sharedInstance;
}
使用dispatch_once可以簡化代碼并且徹底保證線程安全,開發(fā)者根本無須擔心加鎖或同步嚷堡。所有問題都由GCD在底層處理蝗罗。由于每次調(diào)用時都必須使用完全相同的標記,所以標記要聲明稱static麦到。把該變量定義在static作用域里绿饵,可以保證編譯器在每次執(zhí)行sharedInstance方法時都會復用這個變量,而不會創(chuàng)建新變量瓶颠。
此外拟赊,dispatch_once更高效。它沒有使用重量級的同步機制粹淋,若是那樣的話吸祟,每次運行代碼錢都要獲取鎖,相反桃移,此函數(shù)采用“原子訪問”(atomic access)來查詢標記屋匕,以判斷其所對應的代碼原來是否已經(jīng)執(zhí)行過。
要點:
- 經(jīng)常需要編寫“只需執(zhí)行一次的線程安全代碼”(thread-safe single-code execution)借杰。通過GCD所提供的dispatch_once函數(shù)过吻,很容易就能實現(xiàn)此功能。
- 標記應該聲明在static或global作用域中蔗衡,這樣的話纤虽,在把只需執(zhí)行一次的塊傳給dispatch_once函數(shù)時,傳進去的標記也是相同的绞惦。
46.不要使用dispatch_get_current_queue
使用GCD時逼纸,經(jīng)常需要判斷當前代碼正在哪個隊列上執(zhí)行,向多個隊列派發(fā)任務時济蝉,更是如此杰刽。
dispatch_queue_t
dispatch_get_current_queue(void);
此函數(shù)返回當前正在執(zhí)行代碼的隊列菠发,不過用的時候要小心。從iOS系統(tǒng)6.0版本起贺嫂,已經(jīng)將其廢棄了滓鸠。
該函數(shù)有種典型的錯誤用法(antipattern,“反模式”)涝婉,就是用它檢測當前隊列是不是某個特定的隊列哥力,試圖以此來避免執(zhí)行同步派發(fā)時可能遭遇的死鎖問題《胀洌考慮下面這兩個存取方法吩跋,其代碼用隊列來保證對實例變量的訪問操作是同步的:
-(NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
-(void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
這種寫法的問題在于,獲取方法可能會死鎖渔工,假如調(diào)用獲取方法的隊列恰好是同步操作所針對的隊列(本例中是_syncQueue)锌钮,那么dispatch_sync就一直不會返回,直到塊執(zhí)行完畢為止引矩×呵穑可是,應該執(zhí)行塊的那個目標隊列卻是當前隊列旺韭,而當前隊列的dispatch_sync又一直阻塞著氛谜,它在等待目標隊列把這個塊執(zhí)行完,這樣一來区端,塊就永遠沒機會執(zhí)行了值漫。像someString這種方法,就是“不可重入的”织盼。
看了dispatch_get_current_queue的文檔后杨何,你也許會覺得可以用它改寫這個方法,令其變得“可重入”沥邻,只需檢測當前隊列是否為同步操作所針對的隊列危虱,如果是,就不派發(fā)了唐全,直接執(zhí)行塊即可:
-(NSString *)someString{
__block NSString *localSomeString;
dispatch_block_t accessorBlock = ^{
localSomeString = _someString;
};
if(dispatch_get_current_queue()==_syncQueue){
accessorBlock();
}else{
dispatch_sync(_syncQueue, accessorBlock);
}
return localSomeString;
}
這種做法可以處理一些簡單情況埃跷。不過仍然有死鎖的危險。為說明其原因邮利,考慮下面這段代碼弥雹,其中有兩個串行派發(fā)隊列:
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, ^{
//Deadlock
});
});
});
這段代碼執(zhí)行到最內(nèi)層的派發(fā)操作時,總會死鎖近弟,因為此操作是針對queueA隊列的缅糟,所以必須等最外層的dispatch_sync執(zhí)行完畢才行挺智,而最外層的那個dispatch_sync又不可能執(zhí)行完畢祷愉,因為它要等最內(nèi)層的dispatch_sync執(zhí)行完窗宦,于是就死鎖了。現(xiàn)在按照剛才的辦法二鳄,使用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的同步派發(fā)操作依然會執(zhí)行,于是和剛才一樣欺殿,還是死鎖了寄纵。
在這種情況下,正確的做法是:不要把存取方法做成可重入的脖苏,而是應該確保同步操作所用的隊列絕不會訪問屬性程拭,也就是絕不會調(diào)用someString方法。這種隊列只應該用來同步屬性棍潘。由于派發(fā)隊列是一種極為輕量級的機制恃鞋,所以,為了確保每項屬性都有專用的同步隊列亦歉,我們不妨創(chuàng)建多個隊列恤浪。
使用隊列時還要注意另外一個問題,而那個問題會在你意想不到的地方導致死鎖肴楷。隊列之間會形成一套層級體系水由,這意味著排在某條隊列中的塊,會在其上級隊列(parent queue阶祭,也叫“父隊列”)里執(zhí)行绷杜。層級里地位較高的那個隊列總是“全局并發(fā)隊列”。下圖描述了一套簡單的隊列體系:
排在隊列B或隊列C中的塊濒募,稍后會在隊列A里依次執(zhí)行鞭盟。于是,排在隊列A瑰剃、B齿诉、C中的塊總是要彼此錯開執(zhí)行。然而晌姚,安排在隊列D中的塊粤剧,則有可能與隊列A里的塊(也包括隊列B與C里的塊)并行,因為A與D的目標隊列是個并發(fā)隊列挥唠。若有必要抵恋,并發(fā)隊列可以用多個線程并行執(zhí)行多個塊,而是否會這樣做宝磨,則需根據(jù)CPU的核心數(shù)量等系統(tǒng)資源狀況來定弧关。
由于隊列見有層級關系盅安,所以“檢查當前隊列是否為執(zhí)行同步派發(fā)所用的隊列”這種辦法,并不總是奏效世囊。比方說别瞭,排在隊列C里的塊,會認為當前隊列就是隊列C株憾,而開發(fā)者可能會據(jù)此認為:在隊列A上能夠安全地執(zhí)行同步派發(fā)操作蝙寨。但實際上,這么做依然會像前面那樣導致死鎖嗤瞎。
有的API可令開發(fā)者指定運行回調(diào)塊時所用的隊列墙歪,但實際上卻把回調(diào)塊安排在內(nèi)部的串行同步隊列上,而內(nèi)部隊列的目標隊列又是開發(fā)者所提供的那個隊列贝奇,在此情況下箱亿,也許就要出現(xiàn)剛才說的那種問題了。使用這種API的開發(fā)者可能誤以為:在回調(diào)塊里調(diào)用dispatch_get_current_queue所返回的“當前隊列”弃秆,總是其調(diào)用API時指定的那個届惋。但實際上返回的卻是API內(nèi)部的那個同步隊列。
要解決這個問題菠赚,最好的辦法就是通過GCD所提供的功能來設定“隊列特有數(shù)據(jù)”(queue-specific data)脑豹,此功能可以把任意數(shù)據(jù)以鍵值對的形式關聯(lián)到隊列里。最重要之處在于衡查,假如根據(jù)指定的鍵獲取不到關聯(lián)數(shù)據(jù)瘩欺,那么系統(tǒng)就會沿著層級體系向上查找,直至找到數(shù)據(jù)或到達根隊列為止拌牲。看下面這個例子:
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");
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);
}
});
本例創(chuàng)建了兩個隊列俱饿。代碼中將隊列B的目標隊列設為隊列A,而隊列A的目標隊列仍然是默認優(yōu)先級的全局并發(fā)隊列塌忽。然后使用下列函數(shù)拍埠,在隊列A上設置“隊列特定值”:
void
dispatch_queue_set_specific(dispatch_queue_t queue,
const void *key,
void *context,
dispatch_function_t destructor);
此函數(shù)的首個參數(shù)表示待設置數(shù)據(jù)的隊列,其后兩個參數(shù)是鍵與值土居。鍵與值都是不透明的void指針枣购。對于鍵來說,有個問題一定要注意:函數(shù)是按指針值來比較鍵的擦耀,而不是按照其內(nèi)容棉圈。所以,“隊列特定數(shù)據(jù)”更像是“關聯(lián)引用”眷蜓。值(在函數(shù)中原型里叫context)也是不透明的void指針分瘾,于是可以在其中存放任意數(shù)據(jù)。然而吁系,必須管理該對象的內(nèi)存德召。這使得在ARC環(huán)境下很難使用Objective-C對象作為值痊远。范例代碼使用CoreFoundation字符串作為值,因為ARC并不會自動管理CoreFoundation對象的內(nèi)存氏捞。所以說,這種對象非常適合充當“隊列特定數(shù)據(jù)”冒版,它們可以根據(jù)需要與相關的Objective-C Foundation類無縫銜接液茎。
函數(shù)最后一個參數(shù)是“析構(gòu)函數(shù)”,對于給定的鍵來說辞嗡,當隊列所占內(nèi)存為系統(tǒng)所回收捆等,或者有新的值與鍵相關聯(lián)時,原有的值對象就會移除续室,而析構(gòu)函數(shù)也會與于此時執(zhí)行魄眉。dispatch_function_t類的定義如下:
typedef void (*dispatch_function_t)(void *);
由此可知洪乍,析構(gòu)函數(shù)只能帶有一個指針參數(shù)且返回值必須為void。范例代碼采用CFRelease做析構(gòu)函數(shù),此函數(shù)符合要求列疗,不過也可以采用開發(fā)者自定義的函數(shù),在其中調(diào)用CFRelease以清理舊值喷户,并完成其他必要的清理工作隙轻。
于是,“隊列特定數(shù)據(jù)”所提供的這套簡單易用的機制瞳购,就避免了使用dispatch_get_current_queue時經(jīng)常遭遇的一個陷阱话侄。此外,調(diào)試程序時也許會經(jīng)常用到dispatch_get_current_queue学赛。在此情況下年堆,可以放心使用這個已經(jīng)廢棄的方法,只是別把它編譯到發(fā)行版的程序里就行盏浇。
要點:
- dispatch_get_current_queue函數(shù)的行為常常與開發(fā)者所預期的不同变丧。此函數(shù)已經(jīng)廢棄,只應做調(diào)試之用绢掰。
- 由于派發(fā)隊列是按層級來組織的锄贷,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
- dispatch_get_current_queue函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖曼月,然而能用此函數(shù)解決的問題谊却,通常也能改用“隊列特定數(shù)據(jù)”來解決。
轉(zhuǎn)載請注明出處:第六章 block與GCD(下)
參考:《Effective Objective-C 2.0》