1.NSTimer不準(zhǔn)時(shí)的原因:(1).RunLoop循環(huán)處理時(shí)間,每次循環(huán)是固定時(shí)間散罕,只有在這段時(shí)間才會(huì)去查看NSTimer分歇;(2).RunLoop模式會(huì)有影響,設(shè)置模式(這個(gè)不是不準(zhǔn)時(shí)的原因欧漱,只是影響)职抡;
2.GCD-Timer準(zhǔn)時(shí)原因:源不一樣dispatch_source_t
區(qū)別:(1).一個(gè)是RunLoop的源,一個(gè)是dispatch源误甚;GCD不需要加入mode下缚甩,不受模式印象;(2).GCDTimer需要寫全局延長(zhǎng)生命周期窑邦,因?yàn)镽unLoop會(huì)把它釋放擅威;(3).如果GCDTimer寫在主線程也會(huì)受RunLoop影響<看堆棧就可以了>,子線程不會(huì);(4).GCDTimer有開始暫停取消冈钦,nstimer只有開始和取消裕寨,沒有暫停;
GCD介紹(一): 基本概念和Dispatch Queue
同步會(huì)阻塞線程至塊中當(dāng)前線程中的任務(wù)結(jié)束
?異步選擇性開辟線程
?串行隊(duì)列等待執(zhí)行開辟一條線程
?并行隊(duì)列并發(fā)執(zhí)行
1.什么是GCD派继?
Grand Central Dispatch或者GCD宾袜,是一套低層API,提供了一種新的方法來進(jìn)行并發(fā)程序編寫驾窟。從基本功能上講庆猫,GCD有點(diǎn)像NSOperationQueue,他們都允許程序?qū)⑷蝿?wù)切分為多個(gè)單一任務(wù)然后提交至工作隊(duì)列來并發(fā)地或者串行地執(zhí)行绅络。GCD比之NSOpertionQueue更底層更高效月培,并且它不是Cocoa框架的一部分嘁字。
除了代碼的平行執(zhí)行能力,GCD還提供高度集成的事件控制系統(tǒng)杉畜〖脱眩可以設(shè)置句柄來響應(yīng)文件描述符、mach ports(Mach port用于 OS X上的進(jìn)程間通訊)此叠、進(jìn)程纯续、計(jì)時(shí)器、信號(hào)灭袁、用戶生成事件猬错。這些句柄通過GCD來并發(fā)執(zhí)行。
GCD的API很大程度上基于block茸歧,當(dāng)然倦炒,GCD也可以脫離block來使用,比如使用傳統(tǒng)c機(jī)制提供函數(shù)指針和上下文指針软瞎。實(shí)踐證明逢唤,當(dāng)配合block使用時(shí),GCD非常簡(jiǎn)單易用且能發(fā)揮其最大能力涤浇。
你可以在Mac上敲命令“man dispatch”來獲取GCD的文檔智玻。
2.為何使用?
GCD提供很多超越傳統(tǒng)多線程編程的優(yōu)勢(shì):
易用:GCD比之thread跟簡(jiǎn)單易用。由于GCD基于work unit而非像thread那樣基于運(yùn)算芙代,所以GCD可以控制諸如等待任務(wù)結(jié)束、監(jiān)視文件描述符盖彭、周期執(zhí)行代碼以及工作掛起等任務(wù)纹烹。基于block的血統(tǒng)導(dǎo)致它能極為簡(jiǎn)單得在不同代碼作用域之間傳遞上下文召边。
效率:GCD被實(shí)現(xiàn)得輕量?jī)?yōu)雅铺呵,使得它在很多地方比之專門創(chuàng)建消耗資源的線程更實(shí)用且快速。這關(guān)系到易用性:導(dǎo)致GCD易用的原因有一部分在于你可以不用擔(dān)心太多的效率問題而僅僅使用它就行了隧熙。
性能:GCD自動(dòng)根據(jù)系統(tǒng)負(fù)載來增減線程數(shù)量片挂,這就減少了上下文切換以及增加了計(jì)算效率。
Dispatch Objects
盡管GCD是純c語言的贞盯,但它被組建成面向?qū)ο蟮娘L(fēng)格音念。GCD對(duì)象被稱為dispatch object。Dispatch object像Cocoa對(duì)象一樣是引用計(jì)數(shù)的躏敢。使用dispatch_release和dispatch_retain函數(shù)來操作dispatch object的引用計(jì)數(shù)來進(jìn)行內(nèi)存管理闷愤。但注意不像Cocoa對(duì)象,dispatch object并不參與垃圾回收系統(tǒng)件余,所以即使開啟了GC讥脐,你也必須手動(dòng)管理GCD對(duì)象的內(nèi)存遭居。
Dispatch queues 和 dispatch sources(后面會(huì)介紹到)可以被掛起和恢復(fù),可以有一個(gè)相關(guān)聯(lián)的任意上下文指針旬渠,可以有一個(gè)相關(guān)聯(lián)的任務(wù)完成觸發(fā)函數(shù)俱萍。可以查閱“man dispatch_object”來獲取這些功能的更多信息告丢。
Dispatch Queues
GCD的基本概念就是dispatch queue枪蘑。dispatch queue是一個(gè)對(duì)象,它可以接受任務(wù)芋齿,并將任務(wù)以先到先執(zhí)行的順序來執(zhí)行腥寇。dispatch queue可以是并發(fā)的或串行的。并發(fā)任務(wù)會(huì)像NSOperationQueue那樣基于系統(tǒng)負(fù)載來合適地并發(fā)進(jìn)行觅捆,串行隊(duì)列同一時(shí)間只執(zhí)行單一任務(wù)赦役。
3.GCD中有三種隊(duì)列類型:
The main queue:與主線程功能相同。實(shí)際上栅炒,提交至main queue的任務(wù)會(huì)在主線程中執(zhí)行掂摔。main queue可以調(diào)用dispatch_get_main_queue()來獲得。因?yàn)閙ain queue是與主線程相關(guān)的赢赊,所以這是一個(gè)串行隊(duì)列乙漓。
Global queues:全局隊(duì)列是并發(fā)隊(duì)列,并由整個(gè)進(jìn)程共享释移。進(jìn)程中存在三個(gè)全局隊(duì)列:高叭披、中(默認(rèn))、低三個(gè)優(yōu)先級(jí)隊(duì)列玩讳∩可以調(diào)用dispatch_get_global_queue函數(shù)傳入優(yōu)先級(jí)來訪問隊(duì)列。
用戶隊(duì)列:用戶隊(duì)列 (GCD并不這樣稱呼這種隊(duì)列, 但是沒有一個(gè)特定的名字來形容這種隊(duì)列熏纯,所以我們稱其為用戶隊(duì)列) 是用函數(shù)dispatch_queue_create創(chuàng)建的隊(duì)列. 這些隊(duì)列是串行和并行的同诫。正因?yàn)槿绱耍鼈兛梢杂脕硗瓿赏綑C(jī)制, 有點(diǎn)像傳統(tǒng)線程中的mutex樟澜。
創(chuàng)建隊(duì)列
要使用用戶隊(duì)列误窖,我們首先得創(chuàng)建一個(gè)。調(diào)用函數(shù)dispatch_queue_create就行了秩贰。函數(shù)的第一個(gè)參數(shù)是一個(gè)標(biāo)簽霹俺,這純是為了debug。Apple建議我們使用倒置域名來命名隊(duì)列毒费,比如“com.dreamingwish.subsystem.task”吭服。這些名字會(huì)在崩潰日志中被顯示出來兵拢,也可以被調(diào)試器調(diào)用鹏倘,這在調(diào)試中會(huì)很有用。第二個(gè)參數(shù)目前還不支持,傳入NULL就行了患久。
提交 Job
向一個(gè)隊(duì)列提交Job很簡(jiǎn)單:調(diào)用dispatch_async函數(shù)战转,傳入一個(gè)隊(duì)列和一個(gè)block茅姜。隊(duì)列會(huì)在輪到這個(gè)block執(zhí)行時(shí)執(zhí)行這個(gè)block的代碼奏夫。下面的例子是一個(gè)在后臺(tái)執(zhí)行一個(gè)巨長(zhǎng)的任務(wù):
[objc]view plaincopy
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0),?^{??
[self?goDoSomethingLongAndInvolved];??
NSLog(@"Done?doing?something?long?and?involved");??
);??
dispatch_async函數(shù)會(huì)立即返回, block會(huì)在后臺(tái)異步執(zhí)行。
當(dāng)然打瘪,通常友鼻,任務(wù)完成時(shí)簡(jiǎn)單地NSLog個(gè)消息不是個(gè)事兒。在典型的Cocoa程序中闺骚,你很有可能希望在任務(wù)完成時(shí)更新界面彩扔,這就意味著需要在主線程中執(zhí)行一些代碼。你可以簡(jiǎn)單地完成這個(gè)任務(wù)——使用嵌套的dispatch僻爽,在外層中執(zhí)行后臺(tái)任務(wù)虫碉,在內(nèi)層中將任務(wù)dispatch到main queue:
[objc]view plaincopy
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0),?^{??
[self?goDoSomethingLongAndInvolved];??
????????dispatch_async(dispatch_get_main_queue(),?^{??
[textField?setStringValue:@"Done?doing?something?long?and?involved"];??
????????});??
});??
還有一個(gè)函數(shù)叫dispatch_sync,它干的事兒和dispatch_async相同胸梆,但是它會(huì)等待block中的代碼執(zhí)行完成并返回敦捧。結(jié)合 __block類型修飾符,可以用來從執(zhí)行中的block獲取一個(gè)值碰镜。例如兢卵,你可能有一段代碼在后臺(tái)執(zhí)行,而它需要從界面控制層獲取一個(gè)值绪颖。那么你可以使用dispatch_sync簡(jiǎn)單辦到:
[objc]view plaincopy
__block?NSString?*stringValue;??
dispatch_sync(dispatch_get_main_queue(),?^{??
//?__block?variables?aren't?automatically?retained??
//?so?we'd?better?make?sure?we?have?a?reference?we?can?keep??
stringValue?=?[[textField?stringValue]?copy];??
});??
[stringValue?autorelease];??
//?use?stringValue?in?the?background?now??
我們還可以使用更好的方法來完成這件事——使用更“異步”的風(fēng)格秽荤。不同于取界面層的值時(shí)要阻塞后臺(tái)線程,你可以使用嵌套的block來中止后臺(tái)線程柠横,然后從主線程中獲取值窃款,然后再將后期處理提交至后臺(tái)線程:
取決于你的需求,myQueue可以是用戶隊(duì)列也可以使全局隊(duì)列滓鸠。
不再使用鎖(Lock)
用戶隊(duì)列可以用于替代鎖來完成同步機(jī)制。在傳統(tǒng)多線程編程中第喳,你可能有一個(gè)對(duì)象要被多個(gè)線程使用糜俗,你需要一個(gè)鎖來保護(hù)這個(gè)對(duì)象:
訪問代碼會(huì)像這樣:
[objc]view plaincopy
-?(id)something ?{ ?
? ?id?localSomething;??
? [lock?lock]; ?
? localSomething?=?[[something?retain]?autorelease]; ?
? [lock?unlock];??
? return?localSomething;??
}??
-?(void)setSomething:(id)newSomething??
{??
[lock?lock];??
if(newSomething?!=?something)??
????{??
[something?release];??
something?=?[newSomething?retain];??
[self?updateSomethingCaches];??
????}??
? [lock?unlock];??
}??
使用GCD,可以使用queue來替代:
要用于同步機(jī)制曲饱,queue必須是一個(gè)用戶隊(duì)列悠抹,而非全局隊(duì)列,所以使用usingdispatch_queue_create初始化一個(gè)扩淀。然后可以用dispatch_async或者dispatch_sync將共享數(shù)據(jù)的訪問代碼封裝起來:
[objc]view plaincopy
-?(id)something??
{??
__blockid?localSomething;??
????dispatch_sync(queue,?^{??
localSomething?=?[something?retain];??
????});??
return?[localSomething?autorelease];??
}??
-?(void)setSomething:(id)newSomething??
{??
????dispatch_async(queue,?^{??
if(newSomething?!=?something)??
????????{??
[something?release];??
something?=?[newSomething?retain];??
[self?updateSomethingCaches];??
????????}??
????});??
}??
?值得注意的是dispatch queue是非常輕量級(jí)的楔敌,所以你可以大用特用,就像你以前使用lock一樣驻谆。
現(xiàn)在你可能要問:“這樣很好卵凑,但是有意思嗎庆聘?我就是換了點(diǎn)代碼辦到了同一件事兒∩茁”
實(shí)際上伙判,使用GCD途徑有幾個(gè)好處:
平行計(jì)算:注意在第二個(gè)版本的代碼中,-setSomething:是怎么使用dispatch_async的黑忱。調(diào)用-setSomething:會(huì)立即返回宴抚,然后這一大堆工作會(huì)在后臺(tái)執(zhí)行。如果updateSomethingCaches是一個(gè)很費(fèi)時(shí)費(fèi)力的任務(wù)甫煞,且調(diào)用者將要進(jìn)行一項(xiàng)處理器高負(fù)荷任務(wù)菇曲,那么這樣做會(huì)很棒。
安全:使用GCD抚吠,我們就不可能意外寫出具有不成對(duì)Lock的代碼常潮。在常規(guī)Lock代碼中,我們很可能在解鎖之前讓代碼返回了埃跷。使用GCD蕊玷,隊(duì)列通常持續(xù)運(yùn)行,你必將歸還控制權(quán)弥雹。
控制:使用GCD我們可以掛起和恢復(fù)dispatch queue垃帅,而這是基于鎖的方法所不能實(shí)現(xiàn)的。我們還可以將一個(gè)用戶隊(duì)列指向另一個(gè)dspatch queue剪勿,使得這個(gè)用戶隊(duì)列繼承那個(gè)dispatch queue的屬性贸诚。使用這種方法,隊(duì)列的優(yōu)先級(jí)可以被調(diào)整——通過將該隊(duì)列指向一個(gè)不同的全局隊(duì)列厕吉,若有必要的話酱固,這個(gè)隊(duì)列甚至可以被用來在主線程上執(zhí)行代碼。
集成:GCD的事件系統(tǒng)與dispatch queue相集成头朱。對(duì)象需要使用的任何事件或者計(jì)時(shí)器都可以從該對(duì)象的隊(duì)列中指向运悲,使得這些句柄可以自動(dòng)在該隊(duì)列上執(zhí)行,從而使得句柄可以與對(duì)象自動(dòng)同步项钮。
總結(jié)
現(xiàn)在你已經(jīng)知道了GCD的基本概念班眯、怎樣創(chuàng)建dispatch queue、怎樣提交Job至dispatch queue以及怎樣將隊(duì)列用作線程同步烁巫。接下來我會(huì)向你展示如何使用GCD來編寫平行執(zhí)行代碼來充分利用多核系統(tǒng)的性能^ ^署隘。我還會(huì)討論GCD更深層的東西,包括事件系統(tǒng)和queue targeting亚隙。
概念
為了在單一進(jìn)程中充分發(fā)揮多核的優(yōu)勢(shì)磁餐,我們有必要使用多線程技術(shù)(我們沒必要去提多進(jìn)程,這玩意兒和GCD沒關(guān)系)阿弃。在低層诊霹,GCD全局dispatch queue僅僅是工作線程池的抽象羞延。這些隊(duì)列中的Block一旦可用,就會(huì)被dispatch到工作線程中畅哑。提交至用戶隊(duì)列的Block最終也會(huì)通過全局隊(duì)列進(jìn)入相同的工作線程池(除非你的用戶隊(duì)列的目標(biāo)是主線程肴楷,但是為了提高運(yùn)行速度,我們絕不會(huì)這么干)荠呐。
有兩種途徑來通過GCD“榨取”多核心系統(tǒng)的性能:將單一任務(wù)或者一組相關(guān)任務(wù)并發(fā)至全局隊(duì)列中運(yùn)算赛蔫;將多個(gè)不相關(guān)的任務(wù)或者關(guān)聯(lián)不緊密的任務(wù)并發(fā)至用戶隊(duì)列中運(yùn)算;
全局隊(duì)列
設(shè)想下面的循環(huán):
1
2
for(idobj
in array)
????[selfdoSomethingIntensiveWith:obj];
假定-doSomethingIntensiveWith:是線程安全的且可以同時(shí)執(zhí)行多個(gè).一個(gè)array通常包含多個(gè)元素泥张,這樣的話呵恢,我們可以很簡(jiǎn)單地使用GCD來平行運(yùn)算:
1
2
3
4
5
dispatch_queue_t
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
for(idobj
in array)
????dispatch_async(queue,
^{
????????[selfdoSomethingIntensiveWith:obj];
????});
如此簡(jiǎn)單,我們已經(jīng)在多核心上運(yùn)行這段代碼了媚创。?
當(dāng)然這段代碼并不完美渗钉。有時(shí)候我們有一段代碼要像這樣操作一個(gè)數(shù)組,但是在操作完成后钞钙,我們還需要對(duì)操作結(jié)果進(jìn)行其他操作:
1
2
3
for(idobj
in array)
????[selfdoSomethingIntensiveWith:obj];
[selfdoSomethingWith:array];
這時(shí)候使用GCD的dispatch_async就悲劇了.我們還不能簡(jiǎn)單地使用dispatch_sync來解決這個(gè)問題,?因?yàn)檫@將導(dǎo)致每個(gè)迭代器阻塞鳄橘,就完全破壞了平行計(jì)算。
解決這個(gè)問題的一種方法是使用dispatch group芒炼。一個(gè)dispatch group可以用來將多個(gè)block組成一組以監(jiān)測(cè)這些Block全部完成或者等待全部完成時(shí)發(fā)出的消息瘫怜。使用函數(shù)dispatch_group_create來創(chuàng)建,然后使用函數(shù)dispatch_group_async來將block提交至一個(gè)dispatch queue本刽,同時(shí)將它們添加至一個(gè)組鲸湃。所以我們現(xiàn)在可以重新編碼:
1
2
3
4
5
6
7
8
9
10
dispatch_queue_t
queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_group_t
group = dispatch_group_create();
for(idobj
in array)
????dispatch_group_async(group,
queue, ^{
????????[selfdoSomethingIntensiveWith:obj];
????});
dispatch_group_wait(group,
DISPATCH_TIME_FOREVER);
dispatch_release(group);
[selfdoSomethingWith:array];
如果這些工作可以異步執(zhí)行,那么我們可以更風(fēng)騷一點(diǎn)子寓,將函數(shù)-doSomethingWith:放在后臺(tái)執(zhí)行暗挑。我們使用dispatch_group_async函數(shù)建立一個(gè)block在組完成后執(zhí)行:
1
2
3
4
5
6
7
8
9
10
dispatch_queue_t
queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_group_t
group = dispatch_group_create();
for(idobj
in array)
????dispatch_group_async(group,
queue, ^{
????????[selfdoSomethingIntensiveWith:obj];
????});
dispatch_group_notify(group,
queue, ^{
????[selfdoSomethingWith:array];
});
dispatch_release(group);
不僅所有數(shù)組元素都會(huì)被平行操作,后續(xù)的操作也會(huì)異步執(zhí)行斜友,并且這些異步運(yùn)算都會(huì)將程序的其他部分的負(fù)載考慮在內(nèi)炸裆。注意如果-doSomethingWith:需要在主線程中執(zhí)行,比如操作GUI鲜屏,那么我們只要將main queue而非全局隊(duì)列傳給dispatch_group_notify函數(shù)就行了烹看。
對(duì)于同步執(zhí)行,GCD提供了一個(gè)簡(jiǎn)化方法叫做dispatch_apply墙歪。這個(gè)函數(shù)調(diào)用單一block多次听系,并平行運(yùn)算贝奇,然后等待所有運(yùn)算結(jié)束虹菲,就像我們想要的那樣:
1
2
3
4
5
dispatch_queue_t
queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
????dispatch_apply([arraycount],
queue, ^(size_t index){
????????[selfdoSomethingIntensiveWith:[arrayobjectAtIndex:index]];
????});
????[selfdoSomethingWith:array];
這很棒,但是異步咋辦掉瞳?dispatch_apply函數(shù)可是沒有異步版本的毕源。但是我們使用的可是一個(gè)為異步而生的API袄四!所以我們只要用dispatch_async函數(shù)將所有代碼推到后臺(tái)就行了:
1
2
3
4
5
6
7
dispatch_queue_t
queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue,
^{
????dispatch_apply([arraycount],
queue, ^(size_t index){
????????[selfdoSomethingIntensiveWith:[arrayobjectAtIndex:index]];
????});
????[selfdoSomethingWith:array];
});
簡(jiǎn)單的要死霎褐!
這種方法的關(guān)鍵在于確定我們的代碼是在一次對(duì)不同的數(shù)據(jù)片段進(jìn)行相似的操作址愿。如果你確定你的任務(wù)是線程安全的(不在本篇討論范圍內(nèi))那么你可以使用GCD來重寫你的循環(huán)了,更平行更風(fēng)騷冻璃。
要看到性能提升响谓,你還得進(jìn)行一大堆工作。比之線程省艳,GCD是輕量和低負(fù)載的娘纷,但是將block提交至queue還是很消耗資源的——block需要被拷貝和入隊(duì),同時(shí)適當(dāng)?shù)墓ぷ骶€程需要被通知跋炕。不要將一張圖片的每個(gè)像素作為一個(gè)block提交至隊(duì)列赖晶,GCD的優(yōu)點(diǎn)就半途夭折了。如果你不確定辐烂,那么請(qǐng)進(jìn)行試驗(yàn)遏插。將程序平行計(jì)算化是一種優(yōu)化措施,在修改代碼之前你必須再三思索纠修,確定修改是有益的(還有確保你修改了正確的地方)胳嘲。
Subsystem并發(fā)運(yùn)算
前面的章節(jié)我們討論了在程序的單個(gè)subsystem中發(fā)揮多核心的優(yōu)勢(shì)。下來我們要跨越多個(gè)子系統(tǒng)分瘾。
例如胎围,設(shè)想一個(gè)程序要打開一個(gè)包含meta信息的文檔。文檔數(shù)據(jù)本身需要解析并轉(zhuǎn)換至模型對(duì)象來顯示德召,meta信息也需要解析和轉(zhuǎn)換白魂。但是,文檔數(shù)據(jù)和meta信息不需要交互上岗。我們可以為文檔和meta各創(chuàng)建一個(gè)dispatch queue福荸,然后并發(fā)執(zhí)行。文檔和meta的解析代碼都會(huì)各自串行執(zhí)行肴掷,從而不用考慮線程安全(只要沒有文檔和meta之間共享的數(shù)據(jù))敬锐,但是它們還是并發(fā)執(zhí)行的。
一旦文檔打開了呆瞻,程序需要響應(yīng)用戶操作台夺。例如,可能需要進(jìn)行拼寫檢查痴脾、代碼高亮颤介、字?jǐn)?shù)統(tǒng)計(jì)、自動(dòng)保存或者其他什么。如果每個(gè)任務(wù)都被實(shí)現(xiàn)為在不同的dispatch queue中執(zhí)行滚朵,那么這些任務(wù)會(huì)并發(fā)執(zhí)行冤灾,并各自將其他任務(wù)的運(yùn)算考慮在內(nèi)(respect to each other),從而省去了多線程編程的麻煩辕近。
使用dispatch source(下次我會(huì)講到)韵吨,我們可以讓GCD將事件直接傳遞給用戶隊(duì)列。例如移宅,程序中監(jiān)視socket連接的代碼可以被置于它自己的dispatch queue中归粉,這樣它會(huì)異步執(zhí)行,并且執(zhí)行時(shí)會(huì)將程序其他部分的運(yùn)算考慮在內(nèi)漏峰。另外盏浇,如果使用用戶隊(duì)列的話,這個(gè)模塊會(huì)串行執(zhí)行芽狗,簡(jiǎn)化程序绢掰。
結(jié)論
我們討論了如何使用GCD來提升程序性能以及發(fā)揮多核系統(tǒng)的優(yōu)勢(shì)。盡管我們需要比較謹(jǐn)慎地編寫并發(fā)程序童擎,GCD還是使得我們能更簡(jiǎn)單地發(fā)揮系統(tǒng)的可用計(jì)算資源滴劲。
下一篇中,我們將討論dispatch source顾复,也就是GCD的監(jiān)視內(nèi)部班挖、外部事件的機(jī)制。
何為Dispatch Sources
簡(jiǎn)單來說芯砸,dispatch source是一個(gè)監(jiān)視某些類型事件的對(duì)象萧芙。當(dāng)這些事件發(fā)生時(shí),它自動(dòng)將一個(gè)block放入一個(gè)dispatch queue的執(zhí)行例程中假丧。
說的貌似有點(diǎn)不清不楚双揪。我們到底討論哪些事件類型?
下面是GCD 10.6.0版本支持的事件:
Mach port send right state changes.
Mach port receive right state changes.
External process state change.
File descriptor ready for read.
File descriptor ready for write.
Filesystem node event.
POSIX signal.
Custom timer.
Custom event.
這是一堆很有用的東西包帚,它支持所有kqueue所支持的事件(kqueue是什么渔期?見http://en.wikipedia.org/wiki/Kqueue)以及mach(mach是什么?見http://en.wikipedia.org/wiki/Mach_(kernel))端口渴邦、內(nèi)建計(jì)時(shí)器支持(這樣我們就不用使用超時(shí)參數(shù)來創(chuàng)建自己的計(jì)時(shí)器)和用戶事件疯趟。
用戶事件
這些事件里面多數(shù)都可以從名字中看出含義,但是你可能想知道啥叫用戶事件谋梭。簡(jiǎn)單地說信峻,這種事件是由你調(diào)用dispatch_source_merge_data函數(shù)來向自己發(fā)出的信號(hào)。
這個(gè)名字對(duì)于一個(gè)發(fā)出事件信號(hào)的函數(shù)來說瓮床,太怪異了盹舞。這個(gè)名字的來由是GCD會(huì)在事件句柄被執(zhí)行之前自動(dòng)將多個(gè)事件進(jìn)行聯(lián)結(jié)姨夹。你可以將數(shù)據(jù)“拼接”至dispatch source中任意次,并且如果dispatch queue在這期間繁忙的話矾策,GCD只會(huì)調(diào)用該句柄一次(不要覺得這樣會(huì)有問題,看完下面的內(nèi)容你就明白了)峭沦。
用戶事件有兩種:DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_DATA_OR.用戶事件源有個(gè)unsigned
long data屬性贾虽,我們將一個(gè)unsigned long傳入dispatch_source_merge_data。當(dāng)使用_ADD版本時(shí)吼鱼,事件在聯(lián)結(jié)時(shí)會(huì)把這些數(shù)字相加蓬豁。當(dāng)使用_OR版本時(shí),事件在聯(lián)結(jié)時(shí)會(huì)把這些數(shù)字邏輯與運(yùn)算菇肃。當(dāng)事件句柄執(zhí)行時(shí)地粪,我們可以使用dispatch_source_get_data函數(shù)訪問當(dāng)前值,然后這個(gè)值會(huì)被重置為0琐谤。
讓我假設(shè)一種情況蟆技。假設(shè)一些異步執(zhí)行的代碼會(huì)更新一個(gè)進(jìn)度條。因?yàn)橹骶€程只不過是GCD的另一個(gè)dispatch queue而已斗忌,所以我們可以將GUI更新工作push到主線程中质礼。然而,這些事件可能會(huì)有一大堆织阳,我們不想對(duì)GUI進(jìn)行頻繁而累贅的更新眶蕉,理想的情況是當(dāng)主線程繁忙時(shí)將所有的改變聯(lián)結(jié)起來。
用dispatch source就完美了唧躲,使用DISPATCH_SOURCE_TYPE_DATA_ADD造挽,我們可以將工作拼接起來,然后主線程可以知道從上一次處理完事件到現(xiàn)在一共發(fā)生了多少改變弄痹,然后將這一整段改變一次更新至進(jìn)度條饭入。
啥也不說了,上代碼:
[objc]view plaincopy
dispatch_source_t?source?=?dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD,?0,?0,?dispatch_get_main_queue());??
dispatch_source_set_event_handler(source,?^{??
[progressIndicator?incrementBy:dispatch_source_get_data(source)];??
});??
dispatch_resume(source);??
dispatch_apply([array?count],?globalQueue,?^(size_t?index)?{??
//?do?some?work?on?data?at?index??
dispatch_source_merge_data(source,1);??
});??
(對(duì)于這段代碼肛真,我很想說點(diǎn)什么圣拄,我第一次用dispatch source時(shí),我糾結(jié)了很久很久毁欣,真讓人蛋疼:Dispatch source啟動(dòng)時(shí)默認(rèn)狀態(tài)是掛起的庇谆,我們創(chuàng)建完畢之后得主動(dòng)恢復(fù),否則事件不會(huì)被傳遞凭疮,也不會(huì)被執(zhí)行)
假設(shè)你已經(jīng)將進(jìn)度條的min/max值設(shè)置好了饭耳,那么這段代碼就完美了。數(shù)據(jù)會(huì)被并發(fā)處理执解。當(dāng)每一段數(shù)據(jù)完成后寞肖,會(huì)通知dispatch source并將dispatch source data加1纲酗,這樣我們就認(rèn)為一個(gè)單元的工作完成了。事件句柄根據(jù)已完成的工作單元來更新進(jìn)度條新蟆。若主線程比較空閑并且這些工作單元進(jìn)行的比較慢觅赊,那么事件句柄會(huì)在每個(gè)工作單元完成的時(shí)候被調(diào)用,實(shí)時(shí)更新琼稻。如果主線程忙于其他工作吮螺,或者工作單元完成速度很快,那么完成事件會(huì)被聯(lián)結(jié)起來帕翻,導(dǎo)致進(jìn)度條只在主線程變得可用時(shí)才被更新鸠补,并且一次將積累的改變更新至GUI。
現(xiàn)在你可能會(huì)想嘀掸,聽起來倒是不錯(cuò)紫岩,但是要是我不想讓事件被聯(lián)結(jié)呢?有時(shí)候你可能想讓每一次信號(hào)都會(huì)引起響應(yīng)睬塌,什么后臺(tái)的智能玩意兒統(tǒng)統(tǒng)不要泉蝌。啊。揩晴。其實(shí)很簡(jiǎn)單的梨与,別把自己繞進(jìn)去了。如果你想讓每一個(gè)信號(hào)都得到響應(yīng)文狱,那使用dispatch_async函數(shù)不就行了粥鞋。實(shí)際上,使用的dispatch source而不使用dispatch_async的唯一原因就是利用聯(lián)結(jié)的優(yōu)勢(shì)瞄崇。
內(nèi)建事件
上面就是怎樣使用用戶事件呻粹,那么內(nèi)建事件呢?看看下面這個(gè)例子苏研,用GCD讀取標(biāo)準(zhǔn)輸入:
[objc]view plaincopy
dispatch_queue_t?globalQueue?=?dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0);??
dispatch_source_t?stdinSource?=?dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,??
???????????????????????????????????????????????????????STDIN_FILENO,??
0,??
???????????????????????????????????????????????????????globalQueue);??
dispatch_source_set_event_handler(stdinSource,?^{??
char?buf[1024];??
int?len?=?read(STDIN_FILENO,?buf,?sizeof(buf));??
if(len?>?0)??
NSLog(@"Got?data?from?stdin:?%.*s",?len,?buf);??
});??
dispatch_resume(stdinSource);??
?簡(jiǎn)單的要死等浊!因?yàn)槲覀兪褂玫氖侨株?duì)列,句柄自動(dòng)在后臺(tái)執(zhí)行摹蘑,與程序的其他部分并行筹燕,這意味著對(duì)這種情況的提速:事件進(jìn)入程序時(shí),程序正在處理其他事務(wù)衅鹿。
這是標(biāo)準(zhǔn)的UNIX方式來處理事務(wù)的好處撒踪,不用去寫loop。如果使用經(jīng)典的read調(diào)用大渤,我們還得萬分留神制妄,因?yàn)榉祷氐臄?shù)據(jù)可能比請(qǐng)求的少,還得忍受無厘頭的“errors”泵三,比如EINTR(系統(tǒng)調(diào)用中斷)耕捞。使用GCD衔掸,我們啥都不用管,就從這些蛋疼的情況里解脫了俺抽。如果我們?cè)谖募枋龇辛粝铝宋醋x取的數(shù)據(jù)敞映,GCD會(huì)再次調(diào)用我們的句柄。
對(duì)于標(biāo)準(zhǔn)輸入磷斧,這沒什么問題振愿,但是對(duì)于其他文件描述符,我們必須考慮在完成讀寫之后怎樣清除描述符瞳抓。對(duì)于dispatch source還處于活躍狀態(tài)時(shí),我們決不能關(guān)閉描述符伏恐。如果另一個(gè)文件描述符被創(chuàng)建了(可能是另一個(gè)線程創(chuàng)建的)并且新的描述符剛好被分配了相同的數(shù)字孩哑,那么你的dispatch source可能會(huì)在不應(yīng)該的時(shí)候突然進(jìn)入讀寫狀態(tài)。de這個(gè)bug可不是什么好玩的事兒翠桦。
適當(dāng)?shù)那宄绞绞鞘褂胐ispatch_source_set_cancel_handler横蜒,并傳入一個(gè)block來關(guān)閉文件描述符。然后我們使用dispatch_source_cancel來取消dispatch source销凑,使得句柄被調(diào)用丛晌,然后文件描述符被關(guān)閉。
使用其他dispatch source類型也差不多斗幼∨熘耄總的來說,你提供一個(gè)source(mach port蜕窿、文件描述符谋逻、進(jìn)程ID等等)的區(qū)分符來作為diapatch source的句柄。mask參數(shù)通常不會(huì)被使用桐经,但是對(duì)于DISPATCH_SOURCE_TYPE_PROC來說mask指的是我們想要接受哪一種進(jìn)程事件毁兆。然后我們提供一個(gè)句柄,然后恢復(fù)這個(gè)source(前面我加粗字體所說的阴挣,得先恢復(fù))气堕,搞定。dispatch source也提供一個(gè)特定于source的data畔咧,我們使用dispatch_source_get_data函數(shù)來訪問它茎芭。例如,文件描述符會(huì)給出大致可用的字節(jié)數(shù)誓沸。進(jìn)程source會(huì)給出上次調(diào)用之后發(fā)生的事件的mask骗爆。具體每種source給出的data的含義,看man page吧蔽介。
計(jì)時(shí)器
計(jì)時(shí)器事件稍有不同摘投。它們不使用handle/mask參數(shù)煮寡,計(jì)時(shí)器事件使用另外一個(gè)函數(shù)dispatch_source_set_timer來配置計(jì)時(shí)器。這個(gè)函數(shù)使用三個(gè)參數(shù)來控制計(jì)時(shí)器觸發(fā):
start參數(shù)控制計(jì)時(shí)器第一次觸發(fā)的時(shí)刻犀呼。參數(shù)類型是dispatch_time_t幸撕,這是一個(gè)opaque類型,我們不能直接操作它外臂。我們得需要dispatch_time和dispatch_walltime函數(shù)來創(chuàng)建它們坐儿。另外,常量DISPATCH_TIME_NOW和DISPATCH_TIME_FOREVER通常很有用宋光。
interval參數(shù)沒什么好解釋的貌矿。
leeway參數(shù)比較有意思。這個(gè)參數(shù)告訴系統(tǒng)我們需要計(jì)時(shí)器觸發(fā)的精準(zhǔn)程度罪佳。所有的計(jì)時(shí)器都不會(huì)保證100%精準(zhǔn)逛漫,這個(gè)參數(shù)用來告訴系統(tǒng)你希望系統(tǒng)保證精準(zhǔn)的努力程度。如果你希望一個(gè)計(jì)時(shí)器沒五秒觸發(fā)一次赘艳,并且越準(zhǔn)越好酌毡,那么你傳遞0為參數(shù)。另外蕾管,如果是一個(gè)周期性任務(wù)枷踏,比如檢查email,那么你會(huì)希望每十分鐘檢查一次掰曾,但是不用那么精準(zhǔn)旭蠕。所以你可以傳入60,告訴系統(tǒng)60秒的誤差是可接受的旷坦。
這樣有什么意義呢下梢?簡(jiǎn)單來說,就是降低資源消耗塞蹭。如果系統(tǒng)可以讓cpu休息足夠長(zhǎng)的時(shí)間孽江,并在每次醒來的時(shí)候執(zhí)行一個(gè)任務(wù)集合,而不是不斷的醒來睡去以執(zhí)行任務(wù)番电,那么系統(tǒng)會(huì)更高效岗屏。如果傳入一個(gè)比較大的leeway給你的計(jì)時(shí)器,意味著你允許系統(tǒng)拖延你的計(jì)時(shí)器來將計(jì)時(shí)器任務(wù)與其他任務(wù)聯(lián)合起來一起執(zhí)行漱办。
總結(jié)
現(xiàn)在你知道怎樣使用GCD的dispatch source功能來監(jiān)視文件描述符这刷、計(jì)時(shí)器、聯(lián)結(jié)的用戶事件以及其他類似的行為娩井。由于dispatch source完全與dispatch queue相集成暇屋,所以你可以使用任意的dispatch queue。你可以將一個(gè)dispatch source的句柄在主線程中執(zhí)行洞辣、在全局隊(duì)列中并發(fā)執(zhí)行、或者在用戶隊(duì)列中串行執(zhí)行(執(zhí)行時(shí)會(huì)將程序的其他模塊的運(yùn)算考慮在內(nèi))。
下一篇我會(huì)討論如何對(duì)dispatch queue進(jìn)行掛起煞檩、恢復(fù)、重定目標(biāo)操作而涉;如何使用dispatch semaphore;如何使用GCD的一次性初始化功能联予。
Dispatch Queue掛起
dispatch queue可以被掛起和恢復(fù)啼县。使用dispatch_suspend函數(shù)來掛起,使用dispatch_resume函數(shù)來恢復(fù)沸久。這兩個(gè)函數(shù)的行為是如你所愿的季眷。另外,這兩個(gè)函數(shù)也可以用于dispatch source卷胯。
一個(gè)要注意的地方是子刮,dispatch queue的掛起是block粒度的。換句話說诵竭,掛起一個(gè)queue并不會(huì)將當(dāng)前正在執(zhí)行的block掛起话告。它會(huì)允許當(dāng)前執(zhí)行的block執(zhí)行完畢兼搏,然后后續(xù)的block不再會(huì)被執(zhí)行卵慰,直至queue被恢復(fù)。
還有一個(gè)注意點(diǎn):從man頁上得來的:如果你掛起了一個(gè)queue或者source佛呻,那么銷毀它之前裳朋,必須先對(duì)其進(jìn)行恢復(fù)。
Dispatch Queue目標(biāo)指定
所有的用戶隊(duì)列都有一個(gè)目標(biāo)隊(duì)列概念吓著。從本質(zhì)上講鲤嫡,一個(gè)用戶隊(duì)列實(shí)際上是不執(zhí)行任何任務(wù)的,但是它會(huì)將任務(wù)傳遞給它的目標(biāo)隊(duì)列來執(zhí)行绑莺。通常暖眼,目標(biāo)隊(duì)列是默認(rèn)優(yōu)先級(jí)的全局隊(duì)列。
用戶隊(duì)列的目標(biāo)隊(duì)列可以用函數(shù)dispatch_set_target_queue來修改纺裁。我們可以將任意dispatch queue傳遞給這個(gè)函數(shù)诫肠,甚至可以是另一個(gè)用戶隊(duì)列,只要?jiǎng)e構(gòu)成循環(huán)就行欺缘。這個(gè)函數(shù)可以用來設(shè)定用戶隊(duì)列的優(yōu)先級(jí)栋豫。比如我們可以將用戶隊(duì)列的目標(biāo)隊(duì)列設(shè)定為低優(yōu)先級(jí)的全局隊(duì)列,那么我們的用戶隊(duì)列中的任務(wù)都會(huì)以低優(yōu)先級(jí)執(zhí)行谚殊。高優(yōu)先級(jí)也是一樣道理丧鸯。
有一個(gè)用途,是將用戶隊(duì)列的目標(biāo)定為main queue嫩絮。這會(huì)導(dǎo)致所有提交到該用戶隊(duì)列的block在主線程中執(zhí)行丛肢。這樣做來替代直接在主線程中執(zhí)行代碼的好處在于围肥,我們的用戶隊(duì)列可以單獨(dú)地被掛起和恢復(fù),還可以被重定目標(biāo)至一個(gè)全局隊(duì)列摔踱,然后所有的block會(huì)變成在全局隊(duì)列上執(zhí)行(只要你確保你的代碼離開主線程不會(huì)有問題)虐先。
還有一個(gè)用途,是將一個(gè)用戶隊(duì)列的目標(biāo)隊(duì)列指定為另一個(gè)用戶隊(duì)列派敷。這樣做可以強(qiáng)制多個(gè)隊(duì)列相互協(xié)調(diào)地串行執(zhí)行蛹批,這樣足以構(gòu)建一組隊(duì)列,通過掛起和暫停那個(gè)目標(biāo)隊(duì)列篮愉,我們可以掛起和暫停整個(gè)組腐芍。想象這樣一個(gè)程序:它掃描一組目錄并且加載目錄中的內(nèi)容。為了避免磁盤競(jìng)爭(zhēng)试躏,我們要確定在同一個(gè)物理磁盤上同時(shí)只有一個(gè)文件加載任務(wù)在執(zhí)行猪勇。而希望可以同時(shí)從不同的物理磁盤上讀取多個(gè)文件。要實(shí)現(xiàn)這個(gè)颠蕴,我們要做的就是創(chuàng)建一個(gè)dispatch queue結(jié)構(gòu)泣刹,該結(jié)構(gòu)為磁盤結(jié)構(gòu)的鏡像。
首先犀被,我們會(huì)掃描系統(tǒng)并找到各個(gè)磁盤椅您,為每個(gè)磁盤創(chuàng)建一個(gè)用戶隊(duì)列。然后掃描文件系統(tǒng)寡键,并為每個(gè)文件系統(tǒng)創(chuàng)建一個(gè)用戶隊(duì)列掀泳,將這些用戶隊(duì)列的目標(biāo)隊(duì)列指向合適的磁盤用戶隊(duì)列。最后西轩,每個(gè)目錄掃描器有自己的隊(duì)列员舵,其目標(biāo)隊(duì)列指向目錄所在的文件系統(tǒng)的隊(duì)列。目錄掃描器枚舉自己的目錄并為每個(gè)文件向自己的隊(duì)列提交一個(gè)block藕畔。由于整個(gè)系統(tǒng)的建立方式马僻,就使得每個(gè)物理磁盤被串行訪問,而多個(gè)物理磁盤被并行訪問注服。除了隊(duì)列初始化過程韭邓,我們根本不需要手動(dòng)干預(yù)什么東西。
信號(hào)量
dispatch的信號(hào)量是像其他的信號(hào)量一樣的祠汇,如果你熟悉其他多線程系統(tǒng)中的信號(hào)量仍秤,那么這一節(jié)的東西再好理解不過了。
信號(hào)量是一個(gè)整形值并且具有一個(gè)初始計(jì)數(shù)值可很,并且支持兩個(gè)操作:信號(hào)通知和等待诗力。當(dāng)一個(gè)信號(hào)量被信號(hào)通知,其計(jì)數(shù)會(huì)被增加。當(dāng)一個(gè)線程在一個(gè)信號(hào)量上等待時(shí)苇本,線程會(huì)被阻塞(如果有必要的話)袜茧,直至計(jì)數(shù)器大于零,然后線程會(huì)減少這個(gè)計(jì)數(shù)瓣窄。
我們使用函數(shù)dispatch_semaphore_create來創(chuàng)建dispatch信號(hào)量笛厦,使用函數(shù)dispatch_semaphore_signal來信號(hào)通知,使用函數(shù)dispatch_semaphore_wait來等待俺夕。這些函數(shù)的man頁有兩個(gè)很好的例子裳凸,展示了怎樣使用信號(hào)量來同步任務(wù)和有限資源訪問控制。
單次初始化
GCD還提供單次初始化支持劝贸,這個(gè)與pthread中的函數(shù)pthread_once很相似姨谷。GCD提供的方式的優(yōu)點(diǎn)在于它使用block而非函數(shù)指針,這就允許更自然的代碼方式:
這個(gè)特性的主要用途是惰性單例初始化或者其他的線程安全數(shù)據(jù)共享映九。典型的單例初始化技術(shù)看起來像這樣(線程安全的):
[objc]view plaincopy
+?(id)sharedWhatever??
{??
static?Whatever?*whatever?=?nil;??
@synchronized([Whatever?class])??
????{??
if(!whatever)??
whatever?=?[[Whatever?alloc]?init];??
????}??
return?whatever;??
}??
這挺好的梦湘,但是代價(jià)比較昂貴;每次調(diào)用+sharedWhatever函數(shù)都會(huì)付出取鎖的代價(jià)件甥,即使這個(gè)鎖只需要進(jìn)行一次捌议。確實(shí)有更風(fēng)騷的方式來實(shí)現(xiàn)這個(gè),使用類似雙向鎖或者是原子操作的東西引有,但是這樣挺難弄而且容易出錯(cuò)瓣颅。
使用GCD,我們可以這樣重寫上面的方法轿曙,使用函數(shù)dispatch_once:
[objc]view plaincopy
+?(id)sharedWhatever??
{??
static?dispatch_once_t?pred;??
static?Whatever?*whatever?=?nil;??
????dispatch_once(&pred,?^{??
whatever?=?[[Whatever?alloc]?init];??
????});??
return?whatever;??
}??
這個(gè)稍微比@synchronized方法簡(jiǎn)單些弄捕,并且GCD確保以更快的方式完成這些檢測(cè)僻孝,它保證block中的代碼在任何線程通過dispatch_once調(diào)用之前被執(zhí)行导帝,但它不會(huì)強(qiáng)制每次調(diào)用這個(gè)函數(shù)都讓代碼進(jìn)行同步控制。實(shí)際上穿铆,如果你去看這個(gè)函數(shù)所在的頭文件您单,你會(huì)發(fā)現(xiàn)目前它的實(shí)現(xiàn)其實(shí)是一個(gè)宏,進(jìn)行了內(nèi)聯(lián)的初始化測(cè)試荞雏,這意味著通常情況下虐秦,你不用付出函數(shù)調(diào)用的負(fù)載代價(jià),并且會(huì)有更少的同步控制負(fù)載凤优。
結(jié)論
這一章悦陋,我們介紹了dispatch queue的掛起、恢復(fù)和目標(biāo)重定筑辨,以及這些功能的一些用途俺驶。另外,我們還介紹了如何使用dispatch 信號(hào)量和單次初始化功能棍辕。到此暮现,我已經(jīng)完成了GCD如何運(yùn)作以及如何使用的介紹还绘。
GCD實(shí)戰(zhàn)一:使用串行隊(duì)列實(shí)現(xiàn)簡(jiǎn)單的預(yù)加載
其主要思路是使用gcd創(chuàng)建串行隊(duì)列,然后在此隊(duì)列中先后執(zhí)行兩個(gè)任務(wù):1.預(yù)加載一個(gè)viewController 2.將這個(gè)viewController推入
代碼如下:
[objc]view plaincopy
@implementation?DWAppDelegate??
{??
????dispatch_queue_t?_serialQueue;??
UINavigationController?*_navController;??
}??
-?(dispatch_queue_t)serialQueue??
{??
if?(!_serialQueue)?{??
_serialQueue?=?dispatch_queue_create("serialQueue",?DISPATCH_QUEUE_SERIAL);//創(chuàng)建串行隊(duì)列??
????}??
return?_serialQueue;??
}??
-?(void)prepareViewController??
{??
dispatch_async([self?serialQueue],?^{//把block中的任務(wù)放入串行隊(duì)列中執(zhí)行栖袋,這是第一個(gè)任務(wù)??
self.viewController?=?[[[DWViewController?alloc]?init]?autorelease];??
sleep(2);//假裝這個(gè)viewController創(chuàng)建起來很花時(shí)間拍顷。塘幅。其實(shí)view都還沒加載昔案,根本不花時(shí)間。??
NSLog(@"prepared");??
????});??
}??
-?(void)goToViewController??
{??
dispatch_async([self?serialQueue],?^{//第二個(gè)任務(wù)电媳,推入viewController??
NSLog(@"go");??
dispatch_async(dispatch_get_main_queue(),?^{//涉及UI更新的操作爱沟,放入主線程中??
[_navController?pushViewController:self.viewController?animated:YES];??
????????});??
????});??
}??
-?(void)dealloc??
{??
????dispatch_release(_serialQueue);??
[_navController?release];??
[_window?release];??
[_viewController?release];??
[super?dealloc];??
}??
-?(BOOL)application:(UIApplication?*)application?didFinishLaunchingWithOptions:(NSDictionary?*)launchOptions??
{??
[self?prepareViewController];??
self.window?=?[[[UIWindow?alloc]?initWithFrame:[[UIScreen?mainScreen]?bounds]]?autorelease];??
//?Override?point?for?customization?after?application?launch.??
DWViewController?*viewController?=?[[[DWViewController?alloc]?initWithNibName:@"DWViewController"?bundle:nil]?autorelease];??
viewController.view.backgroundColor?=?[UIColor?blueColor];??
_navController?=?[[UINavigationController?alloc]?initWithRootViewController:viewController];??
self.window.rootViewController?=?_navController;??
[self?goToViewController];??
[self.window?makeKeyAndVisible];??
return?YES;??
}??
GCD實(shí)戰(zhàn)2:資源競(jìng)爭(zhēng)
概述
我將分四步來帶大家研究研究程序的并發(fā)計(jì)算。第一步是基本的串行程序匆背,然后使用GCD把它并行計(jì)算化呼伸。如果你想順著步驟來嘗試這些程序的話,可以下載源碼钝尸。注意括享,別運(yùn)行imagegcd2.m,這是個(gè)反面教材珍促。铃辖。
原始程序
我們的程序只是簡(jiǎn)單地遍歷~/Pictures然后生成縮略圖。這個(gè)程序是個(gè)命令行程序猪叙,沒有圖形界面(盡管是使用Cocoa開發(fā)庫的)娇斩,主函數(shù)如下:
[objc]view plaincopy
int?main(int?argc,?charchar?**argv)??
{??
NSAutoreleasePool?*outerPool?=?[NSAutoreleasePool?new];??
????NSApplicationLoad();??
NSString?*destination?=?@"/tmp/imagegcd";??
[[NSFileManager?defaultManager]?removeItemAtPath:?destination?error:?NULL];??
[[NSFileManager?defaultManager]?createDirectoryAtPath:?destination??
?withIntermediateDirectories:?YES??
?attributes:?nil??
?error:?NULL];??
????Start();??
NSString?*dir?=?[@"~/Pictures"?stringByExpandingTildeInPath];??
NSDirectoryEnumerator?*enumerator?=?[[NSFileManager?defaultManager]?enumeratorAtPath:?dir];??
int?count?=?0;??
for(NSString?*path?in?enumerator)??
????{??
NSAutoreleasePool?*innerPool?=?[NSAutoreleasePool?new];??
if([[[path?pathExtension]?lowercaseString]?isEqual:?@"jpg"])??
????????{??
path?=?[dir?stringByAppendingPathComponent:?path];??
NSData?*data?=?[NSData?dataWithContentsOfFile:?path];??
if(data)??
????????????{??
NSData?*thumbnailData?=?ThumbnailDataForData(data);??
if(thumbnailData)??
????????????????{??
NSString?*thumbnailName?=?[NSString?stringWithFormat:?@"%d.jpg",?count++];??
NSString?*thumbnailPath?=?[destination?stringByAppendingPathComponent:?thumbnailName];??
[thumbnailData?writeToFile:?thumbnailPath?atomically:?NO];??
????????????????}??
????????????}??
????????}??
[innerPool?release];??
????}??
????End();??
[outerPool?release];??
}??
如果你要看到所有的副主函數(shù)的話,到文章頂部下載源代碼吧穴翩。當(dāng)前這個(gè)程序是imagegcd1.m犬第。程序中重要的部分都在這里了。.Start函數(shù)和End函數(shù)只是簡(jiǎn)單的計(jì)時(shí)函數(shù)(內(nèi)部實(shí)現(xiàn)是使用的gettimeofday函數(shù))芒帕。ThumbnailDataForData函數(shù)使用Cocoa庫來加載圖片數(shù)據(jù)生成Image對(duì)象歉嗓,然后將圖片縮小到320×320大小,最后將其編碼為JPEG格式背蟆。
簡(jiǎn)單而天真的并發(fā)
乍一看鉴分,我們感覺將這個(gè)程序并發(fā)計(jì)算化,很容易带膀。循環(huán)中的每個(gè)迭代器都可以放入GCD global queue中志珍。我們可以使用dispatch queue來等待它們完成。為了保證每次迭代都會(huì)得到唯一的文件名數(shù)字垛叨,我們使用OSAtomicIncrement32來原子操作級(jí)別的增加count數(shù):
[objc]view plaincopy
dispatch_queue_t?globalQueue?=?dispatch_get_global_queue(0,?0);??
dispatch_group_t?group?=?dispatch_group_create();??
__block?uint32_t?count?=?-1;??
for(NSString?*path?in?enumerator)??
{??
????dispatch_group_async(group,?globalQueue,?BlockWithAutoreleasePool(^{??
if([[[path?pathExtension]?lowercaseString]?isEqual:?@"jpg"])??
????????{??
NSString?*fullPath?=?[dir?stringByAppendingPathComponent:?path];??
NSData?*data?=?[NSData?dataWithContentsOfFile:?fullPath];??
if(data)??
????????????{??
NSData?*thumbnailData?=?ThumbnailDataForData(data);??
if(thumbnailData)??
????????????????{??
NSString?*thumbnailName?=?[NSString?stringWithFormat:?@"%d.jpg",??
OSAtomicIncrement32(&count;)];??
NSString?*thumbnailPath?=?[destination?stringByAppendingPathComponent:?thumbnailName];??
[thumbnailData?writeToFile:?thumbnailPath?atomically:?NO];??
????????????????}??
????????????}??
????????}??
????});??
}??
dispatch_group_wait(group,?DISPATCH_TIME_FOREVER);??
這個(gè)就是imagegcd2.m伦糯,但是,注意,別運(yùn)行這個(gè)程序舔株,有很大的問題莺琳。?
如果你無視我的警告還是運(yùn)行這個(gè)imagegcd2.m了,你現(xiàn)在很有可能是在重啟了電腦后载慈,又打開了我的頁面惭等。。如果你乖乖地沒有運(yùn)行這個(gè)程序的話办铡,運(yùn)行這個(gè)程序發(fā)生的情況就是(如果你有很多很多圖片在~/Pictures中):電腦沒反應(yīng)辞做,好久好久都不動(dòng),假死了寡具。秤茅。
問題在哪
問題出在哪?就在于GCD的智能上童叠。GCD將任務(wù)放到全局線程池中運(yùn)行框喳,這個(gè)線程池的大小根據(jù)系統(tǒng)負(fù)載來隨時(shí)改變。例如厦坛,我的電腦有四核五垮,所以如果我使用GCD加載任務(wù),GCD會(huì)為我每個(gè)cpu核創(chuàng)建一個(gè)線程杜秸,也就是四個(gè)線程放仗。如果電腦上其他任務(wù)需要進(jìn)行的話,GCD會(huì)減少線程數(shù)來使其他任務(wù)得以占用cpu資源來完成撬碟。
但是诞挨,GCD也可以增加活動(dòng)線程數(shù)。它會(huì)在其他某個(gè)線程阻塞時(shí)增加活動(dòng)線程數(shù)呢蛤。假設(shè)現(xiàn)在有四個(gè)線程正在運(yùn)行惶傻,突然某個(gè)線程要做一個(gè)操作,比如顾稀,讀文件达罗,這個(gè)線程就會(huì)等待磁盤響應(yīng)坝撑,此時(shí)cpu核心會(huì)處于未充分利用的狀態(tài)静秆。這是GCD就會(huì)發(fā)現(xiàn)這個(gè)狀態(tài),然后創(chuàng)建另一個(gè)線程來填補(bǔ)這個(gè)資源浪費(fèi)空缺巡李。
現(xiàn)在抚笔,想想上面的程序發(fā)生了啥?主線程非常迅速地將任務(wù)不斷放入global queue中侨拦。GCD以一個(gè)少量工作線程的狀態(tài)開始殊橙,然后開始執(zhí)行任務(wù)。這些任務(wù)執(zhí)行了一些很輕量的工作后,就開始等待磁盤資源膨蛮,慢得不像話的磁盤資源叠纹。
我們別忘記磁盤資源的特性,除非你使用的是SSD或者牛逼的RAID敞葛,否則磁盤資源會(huì)在競(jìng)爭(zhēng)的時(shí)候變得異常的慢誉察。。
剛開始的四個(gè)任務(wù)很輕松地就同時(shí)訪問到了磁盤資源惹谐,然后開始等待磁盤資源返回持偏。這時(shí)GCD發(fā)現(xiàn)CPU開始空閑了,它繼續(xù)增加工作線程氨肌。然后鸿秆,這些線程執(zhí)行更多的磁盤讀取任務(wù),然后GCD再創(chuàng)建更多的工資線程怎囚。卿叽。。
可能在某個(gè)時(shí)間文件讀取任務(wù)有完成的了】沂兀現(xiàn)在附帽,線程池中可不止有四個(gè)線程,相反井誉,有成百上千個(gè)蕉扮。。颗圣。GCD又會(huì)嘗試將工作線程減少(太多使用CPU資源的線程)喳钟,但是減少線程是由條件的,GCD不可以將一個(gè)正在執(zhí)行任務(wù)的線程殺掉在岂,并且也不能將這樣的任務(wù)暫停奔则。它必須等待這個(gè)任務(wù)完成。所有這些情況都導(dǎo)致GCD無法減少工作線程數(shù)蔽午。
然后所有這上百個(gè)線程開始一個(gè)個(gè)完成了他們的磁盤讀取工作易茬。它們開始競(jìng)爭(zhēng)CPU資源,當(dāng)然CPU在處理競(jìng)爭(zhēng)上比磁盤先進(jìn)多了及老。問題在于抽莱,這些線程讀完文件后開始編碼這些圖片,如果你有很多很多圖片骄恶,那么你的內(nèi)存將開始爆倉食铐。。然后內(nèi)存耗盡咋辦僧鲁?虛擬內(nèi)存啊虐呻,虛擬內(nèi)存是啥象泵,磁盤資源啊。Oh shit斟叼!~
然后進(jìn)入了一個(gè)惡性循環(huán)偶惠,磁盤資源競(jìng)爭(zhēng)導(dǎo)致更多的線程被創(chuàng)建,這些線程導(dǎo)致更多的內(nèi)存使用朗涩,然后內(nèi)存爆倉導(dǎo)致虛擬內(nèi)存交換洲鸠,直至GCD創(chuàng)建了系統(tǒng)規(guī)定的線程數(shù)上限(可能是512個(gè)),而這些線程又沒法被殺掉或暫停馋缅。扒腕。。
這就是使用GCD時(shí)萤悴,要注意的瘾腰。GCD能智能地根據(jù)CPU情況來調(diào)整工作線程數(shù),但是它卻無法監(jiān)視其他類型的資源狀況覆履。如果你的任務(wù)牽涉大量IO或者其他會(huì)導(dǎo)致線程block的東西蹋盆,你需要把握好這個(gè)問題。
修正
問題的根源來自于磁盤IO硝全,然后導(dǎo)致惡性循環(huán)栖雾。解決了磁盤資源碰撞,就解決了這個(gè)問題伟众。
GCD的custom queue使得這個(gè)問題易于解決析藕。Custom queue是串行的。如果我們創(chuàng)建一個(gè)custom queue然后將所有的文件讀寫任務(wù)放入這個(gè)隊(duì)列凳厢,磁盤資源的同時(shí)訪問數(shù)會(huì)大大降低账胧,資源訪問碰撞就避免了。
蝦米是我們修正后的代碼先紫,使用IO queue(也就是我們創(chuàng)建的custom queue專門用來讀寫磁盤):
[objc]view plaincopy
dispatch_queue_t?globalQueue?=?dispatch_get_global_queue(0,?0);??
dispatch_queue_t?ioQueue?=?dispatch_queue_create("com.mikeash.imagegcd.io",?NULL);??
dispatch_group_t?group?=?dispatch_group_create();??
__block?uint32_t?count?=?-1;??
for(NSString?*path?in?enumerator)??
{??
if([[[path?pathExtension]?lowercaseString]?isEqual:?@"jpg"])??
????{??
NSString?*fullPath?=?[dir?stringByAppendingPathComponent:?path];??
????????dispatch_group_async(group,?ioQueue,?BlockWithAutoreleasePool(^{??
NSData?*data?=?[NSData?dataWithContentsOfFile:?fullPath];??
if(data)??
????????????????dispatch_group_async(group,?globalQueue,?BlockWithAutoreleasePool(^{??
NSData?*thumbnailData?=?ThumbnailDataForData(data);??
if(thumbnailData)??
????????????????????{??
NSString?*thumbnailName?=?[NSString?stringWithFormat:?@"%d.jpg",??
OSAtomicIncrement32(&count;)];??
NSString?*thumbnailPath?=?[destination?stringByAppendingPathComponent:?thumbnailName];??
????????????????????????dispatch_group_async(group,?ioQueue,?BlockWithAutoreleasePool(^{??
[thumbnailData?writeToFile:?thumbnailPath?atomically:?NO];??
????????????????????????}));??
????????????????????}??
????????????????}));??
????????}));??
????}??
}??
dispatch_group_wait(group,?DISPATCH_TIME_FOREVER);??
這個(gè)就是我們的imagegcd3.m.
GCD使得我們很容易就將任務(wù)的不同部分放入相同的隊(duì)列中去(簡(jiǎn)單地嵌套一下dispatch)治泥。這次我們的程序?qū)?huì)表現(xiàn)地很好。遮精。居夹。我是說多數(shù)情況。本冲。准脂。。
問題在于任務(wù)中的不同部分不是同步的眼俊,導(dǎo)致了整個(gè)程序的不穩(wěn)定意狠。我們的新程序的整個(gè)流程如下:
圖中的箭頭是非阻塞的,并且會(huì)簡(jiǎn)單地將內(nèi)存中的對(duì)象進(jìn)行緩沖疮胖。
?現(xiàn)在假設(shè)一個(gè)機(jī)器的磁盤足夠快环戈,快到比CPU處理任務(wù)(也就是圖片處理)要快。其實(shí)不難想象:雖然CPU的動(dòng)作很快澎灸,但是它的工作更繁重院塞,解碼、壓縮性昭、編碼拦止。從磁盤讀取的數(shù)據(jù)開始填滿IO queue,數(shù)據(jù)會(huì)占用內(nèi)存糜颠,很可能越占越多(如果你的~/Pictures中有很多很多圖片的話)汹族。
然后你就會(huì)內(nèi)存爆倉,然后開始虛擬內(nèi)存交換其兴。顶瞒。。又來了元旬。榴徐。
這就會(huì)像第一次一樣導(dǎo)致惡性循環(huán)。一旦任何東西導(dǎo)致工作線程阻塞匀归,GCD就會(huì)創(chuàng)建更多的線程坑资,這個(gè)線程執(zhí)行的任務(wù)又會(huì)占用內(nèi)存(從磁盤讀取的數(shù)據(jù)),然后又開始交換內(nèi)存穆端。袱贮。
結(jié)果:這個(gè)程序要么就是運(yùn)行地很順暢,要么就是很低效体啰。
注意如果磁盤速度比較慢的話字柠,這個(gè)問題依舊會(huì)出現(xiàn),因?yàn)榭s略圖會(huì)被緩沖在內(nèi)存里狡赐,不過這個(gè)問題導(dǎo)致的低效比較不容易出現(xiàn)窑业,因?yàn)榭s略圖占的內(nèi)存少得多。
真正的修復(fù)
由于上一次我們的嘗試出現(xiàn)的問題在于沒有同步不同部分的操作枕屉,所以讓我寫出同步的代碼常柄。最簡(jiǎn)單的方法就是使用信號(hào)量來限制同時(shí)執(zhí)行的任務(wù)數(shù)量。
那么搀擂,我們需要限制為多少呢西潘?
顯然我們需要根據(jù)CPU的核數(shù)來限制這個(gè)量,我們又想馬兒好又想馬兒不吃草哨颂,我們就設(shè)置為cpu核數(shù)的兩倍吧喷市。不過這里只是簡(jiǎn)單地這樣處理,GCD的作用之一就是讓我們不用關(guān)心操作系統(tǒng)的內(nèi)部信息(比如cpu數(shù))威恼,現(xiàn)在又來讀取cpu核數(shù)品姓,確實(shí)不太妙寝并。也許我們?cè)趯?shí)際應(yīng)用中,可以根據(jù)其他需求來定義這個(gè)限制量腹备。
現(xiàn)在我們的主循環(huán)代碼就是這樣了:
[objc]view plaincopy
dispatch_queue_t?ioQueue?=?dispatch_queue_create("com.mikeash.imagegcd.io",?NULL);??
int?cpuCount?=?[[NSProcessInfo?processInfo]?processorCount];??
dispatch_semaphore_t?jobSemaphore?=?dispatch_semaphore_create(cpuCount?*?2);??
dispatch_group_t?group?=?dispatch_group_create();??
__block?uint32_t?count?=?-1;??
for(NSString?*path?in?enumerator)??
{??
????WithAutoreleasePool(^{??
if([[[path?pathExtension]?lowercaseString]?isEqual:?@"jpg"])??
????????{??
NSString?*fullPath?=?[dir?stringByAppendingPathComponent:?path];??
????????????dispatch_semaphore_wait(jobSemaphore,?DISPATCH_TIME_FOREVER);??
????????????dispatch_group_async(group,?ioQueue,?BlockWithAutoreleasePool(^{??
NSData?*data?=?[NSData?dataWithContentsOfFile:?fullPath];??
????????????????dispatch_group_async(group,?globalQueue,?BlockWithAutoreleasePool(^{??
NSData?*thumbnailData?=?ThumbnailDataForData(data);??
if(thumbnailData)??
????????????????????{??
NSString?*thumbnailName?=?[NSString?stringWithFormat:?@"%d.jpg",??
OSAtomicIncrement32(&count;)];??
NSString?*thumbnailPath?=?[destination?stringByAppendingPathComponent:?thumbnailName];??
????????????????????????dispatch_group_async(group,?ioQueue,?BlockWithAutoreleasePool(^{??
[thumbnailData?writeToFile:?thumbnailPath?atomically:?NO];??
????????????????????????????dispatch_semaphore_signal(jobSemaphore);??
????????????????????????}));??
????????????????????}??
else??
????????????????????????dispatch_semaphore_signal(jobSemaphore);??
????????????????}));??
????????????}));??
????????}??
????});??
}??
dispatch_group_wait(group,?DISPATCH_TIME_FOREVER);??
最終我們寫出了一個(gè)能平滑運(yùn)行且又快速處理的程序衬潦。
基準(zhǔn)測(cè)試
我測(cè)試了一些運(yùn)行時(shí)間,對(duì)7913張圖片:
程序處理時(shí)間 (秒)
imagegcd1.m984
imagegcd2.m沒運(yùn)行植酥,這個(gè)還是別運(yùn)行了
imagegcd3.m300
imagegcd4.m279
注意镀岛,因?yàn)槲冶容^懶。所以我在運(yùn)行這些測(cè)試的時(shí)候友驮,沒有關(guān)閉電腦上的其他程序漂羊。。卸留。嚴(yán)格的進(jìn)行對(duì)照的話走越,實(shí)在是太蛋疼了。艾猜。
所以這個(gè)數(shù)值我們只是參考一下买喧。
比較有意思的是,3和4的執(zhí)行狀況差不多匆赃,大概是因?yàn)槲译娔X有15g可用內(nèi)存吧淤毛。。算柳。內(nèi)存比較小的話低淡,這個(gè)imagegcd3應(yīng)該跑的很吃力,因?yàn)槲野l(fā)現(xiàn)它使用最多的時(shí)候瞬项,占用了10g內(nèi)存蔗蹋。而4的話,沒有占多少內(nèi)存囱淋。
結(jié)論
GCD是個(gè)比較范特西的技術(shù)猪杭,可以辦到很多事兒,但是它不能為你辦所有的事兒妥衣。所以皂吮,對(duì)于進(jìn)行IO操作并且可能會(huì)使用大量?jī)?nèi)存的任務(wù),我們必須仔細(xì)斟酌税手。當(dāng)然蜂筹,即使這樣,GCD還是為我們提供了簡(jiǎn)單有效的方法來進(jìn)行并發(fā)計(jì)算芦倒。