iOS GCD多線程

程序中同步和異步是什么意思?有什么區(qū)別?

  • 解釋一:
    異步調(diào)用是通過使用單獨(dú)的線程執(zhí)行的。原始線程啟動(dòng)異步調(diào)用,異步調(diào)用使用另一個(gè)線程執(zhí)行請(qǐng)求,而與此同時(shí)原始的線程繼續(xù)處理。
    同步調(diào)用則在繼續(xù)之前必須等待響應(yīng)或返回值。如果不允許調(diào)用繼續(xù)即無響應(yīng)或返回值,就說調(diào)用被阻塞了,不能繼續(xù)執(zhí)行。

  • 解釋二:
    同步.一條馬路,只能開一輛車,等這個(gè)車開走了,才能開另一輛.
    異步.一條馬路,隨便開多少車.

  • 解釋三:
    就好比說,向服務(wù)器端發(fā)送請(qǐng)求,如果不需要服務(wù)器響應(yīng)就是異步,如果需要服務(wù)器返回信息就是同步

  • 解釋四:

  • 同步,
    主機(jī)A發(fā)送數(shù)據(jù)的時(shí)候,主機(jī)B必須等待接收,處于阻塞狀態(tài),這就好比別人給你打電話,你必須當(dāng)場(chǎng)聽話,否則則【錯(cuò)失良機(jī)】

  • 異步
    主機(jī)A發(fā)送數(shù)據(jù)的時(shí)候,主機(jī)B無須等待接收,主機(jī)B要獲得數(shù)據(jù)就從緩存里取,就好比別人給你發(fā)郵件一樣

串行執(zhí)行相當(dāng)于同步
并發(fā)執(zhí)行相當(dāng)于異步

GCD介紹(一): 基本概念和Dispatch Queue

GCD提供很多超越傳統(tǒng)多線程編程的優(yōu)勢(shì):

易用: GCD比之thread跟簡單易用。由于GCD基于work unit而非像thread那樣基于運(yùn)算,所以GCD可以控制諸如等待任務(wù)結(jié)束、監(jiān)視文件描述符、周期執(zhí)行代碼以及工作掛起等任務(wù)。基于block的血統(tǒng)導(dǎo)致它能極為簡單得在不同代碼作用域之間傳遞上下文。
效率: GCD被實(shí)現(xiàn)得如此輕量和優(yōu)雅,使得它在很多地方比之專門創(chuàng)建消耗資源的線程更實(shí)用且快速。這關(guān)系到易用性:導(dǎo)致GCD易用的原因有一部分在于你可以不用擔(dān)心太多的效率問題而僅僅使用它就行了。
性能: GCD自動(dòng)根據(jù)系統(tǒng)負(fù)載來增減線程數(shù)量,這就減少了上下午切換以及增加了計(jì)算效率。

Dispatch Objects

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),所以即使開啟了ARC,你也必須手動(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ù)。

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ù)。

GCD中有三種隊(duì)列類型:
  1. 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ì)列。
  2. 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ì)列。
  3. 用戶隊(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很簡單:調(diào)用dispatch_async函數(shù),傳入一個(gè)隊(duì)列和一個(gè)block。隊(duì)列會(huì)在輪到這個(gè)block執(zhí)行時(shí)執(zhí)行這個(gè)block的代碼。下面的例子是一個(gè)在后臺(tái)執(zhí)行一個(gè)巨長的任務(wù):

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í)簡單地NSLog個(gè)消息不是個(gè)事兒。在典型的Cocoa程序中,你很有可能希望在任務(wù)完成時(shí)更新界面,這就意味著需要在主線 程中執(zhí)行一些代碼。你可以簡單地完成這個(gè)任務(wù)——使用嵌套的dispatch,在外層中執(zhí)行后臺(tái)任務(wù),在內(nèi)層中將任務(wù)dispatch到main queue:

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簡單辦到:

__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)線程:

dispatch_queue_t bgQueue = myQueue;

dispatch_async(dispatch_get_main_queue(), ^{

    NSString *stringValue = [[[textField stringValue] copy] autorelease];

    dispatch_async(bgQueue, ^{

        // use stringValue in the background now

    });

});

取決于你的需求,myQueue可以是用戶隊(duì)列也可以使全局隊(duì)列。

不再使用鎖(Lock)

用戶隊(duì)列可以用于替代鎖來完成同步機(jī)制。在傳統(tǒng)多線程編程中,你可能有一個(gè)對(duì)象要被多個(gè)線程使用,你需要一個(gè)鎖來保護(hù)這個(gè)對(duì)象:

NSLock *lock;

訪問代碼會(huì)像這樣:

- (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];

}

值得注意的是dispatch queue是非常輕量級(jí)的,所以你可以大用特用,就像你以前使用lock一樣。

現(xiàn)在你可能要問:“這樣很好,但是有意思嗎?我就是換了點(diǎn)代碼辦到了同一件事兒。”

實(shí)際上,使用GCD途徑有幾個(gè)好處:
  1. 平行計(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ì)很棒。
  2. 安全: 使用GCD,我們就不可能意外寫出具有不成對(duì)Lock的代碼。在常規(guī)Lock代碼中,我們很可能在解鎖之前讓代碼返回了。使用GCD,隊(duì)列通常持續(xù)運(yùn)行,你必將歸還控制權(quán)。
  3. 控制: 使用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í)行 代碼。
  4. 集成: 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。

GCD介紹(二): 多核心的性能

概念

為了在單一進(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):

for(id obj in array)
[self doSomethingIntensiveWith:obj];

假定 -doSomethingIntensiveWith: 是線程安全的且可以同時(shí)執(zhí)行多個(gè).一個(gè)array通常包含多個(gè)元素,這樣的話,我們可以很簡單地使用GCD來平行運(yùn)算:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for(id obj in array)

dispatch_async(queue, ^{

    [self doSomethingIntensiveWith:obj];

});

如此簡單,我們已經(jīng)在多核心上運(yùn)行這段代碼了。

當(dāng)然這段代碼并不完美。有時(shí)候我們有一段代碼要像這樣操作一個(gè)數(shù)組,但是在操作完成后,我們還需要對(duì)操作結(jié)果進(jìn)行其他操作:

for(id obj in array)

[self doSomethingIntensiveWith:obj];

[self doSomethingWith:array];

這時(shí)候使用GCD的 dispatch_async 就悲劇了.我們還不能簡單地使用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)在可以重新代碼:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

for(id obj in array)
dispatch_group_async(group, queue, ^{

    [self doSomethingIntensiveWith:obj];

});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_release(group);
[self doSomethingWith:array];

如果這些工作可以異步執(zhí)行,那么我們可以更風(fēng)騷一點(diǎn),將函數(shù)-doSomethingWith:放在后臺(tái)執(zhí)行。我們使用dispatch_group_async函數(shù)建立一個(gè)block在組完成后執(zhí)行:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

for(id obj in array)

dispatch_group_async(group, queue, ^{

    [self doSomethingIntensiveWith:obj];

});

dispatch_group_notify(group, queue, ^{

[self doSomethingWith:array];

});

dispatch_release(group);

不僅所有數(shù)組元素都會(huì)被平行操作,后續(xù)的操作也會(huì)異步執(zhí)行,并且這些異步運(yùn)算都會(huì)將程序的其他部分考慮在內(nèi)。注意如果-doSomethingWith:需要在主線程中執(zhí)行,比如操作GUI,那么我們只要將main queue而非全局隊(duì)列傳給dispatch_group_notify函數(shù)就行了。

對(duì)于同步執(zhí)行,GCD提供了一個(gè)簡化方法叫做dispatch_apply。這個(gè)函數(shù)調(diào)用單一block多次,并平行運(yùn)算,然后等待所有運(yùn)算結(jié)束,就像我們想要的那樣:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply([array count], queue, ^(size_t index){

    [self doSomethingIntensiveWith:[array objectAtIndex:index]];

});

[self doSomethingWith:array];

這很棒,但是異步咋辦?dispatch_apply函數(shù)可是沒有異步版本的。但是我們使用的可是一個(gè)為異步而生的API啊!所以我們只要用dispatch_async函數(shù)將所有代碼推到后臺(tái)就行了:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{

dispatch_apply([array count], queue, ^(size_t index){

    [self doSomethingIntensiveWith:[array objectAtIndex:index]];

});

[self doSomethingWith:array];

});

簡單的要死!

這種方法的關(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é)論

我們討論了如何使用GCD來提升程序性能以及發(fā)揮多核系統(tǒng)的優(yōu)勢(shì)。盡管我們需要比較謹(jǐn)慎地編寫并發(fā)程序,GCD還是使得我們能更簡單地發(fā)揮系統(tǒng)的可用計(jì)算資源。

下一篇中,我們將討論dispatch source,也就是GCD的監(jiān)視內(nèi)部、外部事件的機(jī)制。

GCD介紹(三): Dispatch Sources

何為Dispatch Sources

簡單來說,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ù)都可以從名字中看出含義,但是你可能想知道啥叫用戶事件。簡單地說,這種事件是由你調(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)度條。

啥也不說了,上代碼:

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 queue啟動(dòng)時(shí)默認(rèn)狀態(tài)是掛起的,我們創(chuàng)建完畢之后得主動(dòng)恢復(fù),否則事件不會(huì)被傳送)

假設(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í)很簡單的,把你的思想放到禁錮的框子之外就行了。如果你想讓每一個(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)輸入:

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);

簡單的要死!因?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ù)那宄绞绞鞘褂?dispatch_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秒的誤差是可接受的。

這樣有什么意義呢?簡單來說,就是降低資源消耗。如果系統(tǒng)可以讓cpu休息足夠長的時(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的一次性初始化功能。

GCD介紹(四): 完結(jié)

Dispatch Queue掛起

dispatch queue可以被掛起和恢復(fù)。使用 dispatch_suspend函數(shù)來掛起,使用 dispatch_resume 函數(shù)來恢復(fù)。這兩個(gè)函數(shù)的行為是如你所愿的。另外,這兩個(gè)還是也可以用于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ù)看起來像這樣(線程安全的):

+ (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:

+ (id)sharedWhatever

{

    static dispatch_once_t pred;

    static Whatever *whatever = nil;

    dispatch_once(&pred, ^{

        whatever = [[Whatever alloc] init];

    });

    return whatever;

}

這個(gè)稍微比 @synchronized方法簡單些,并且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)作以及如何使用的介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • iOS的三種多線程技術(shù) NSThread 每個(gè)NSThread對(duì)象對(duì)應(yīng)一個(gè)線程,量級(jí)較輕(真正的多線程) 以下兩點(diǎn)...
    7分醉閱讀 513評(píng)論 0 4
  • 本篇博客共分以下幾個(gè)模塊來介紹GCD的相關(guān)內(nèi)容: 多線程相關(guān)概念 多線程編程技術(shù)的優(yōu)缺點(diǎn)比較? GCD中的三種隊(duì)列...
    有夢(mèng)想的老伯伯閱讀 1,031評(píng)論 0 4
  • 目錄(GCD): 關(guān)鍵詞 混淆點(diǎn) 場(chǎng)景應(yīng)用 總結(jié) 1. 關(guān)鍵詞 線程概念: 獨(dú)立執(zhí)行的代碼段,一個(gè)線程同時(shí)間只能執(zhí)...
    Ryan___閱讀 1,290評(píng)論 0 3
  • 他是誰?也許只是夢(mèng)中的影,迷糊中只記得: 那一次,我對(duì)他說:“我一直在等待,一次意外,能讓我豁然開朗。”他說:“不...
    十點(diǎn)睡前故事閱讀 363評(píng)論 0 1
  • 這幾天,上下班的路上,在微信讀書里聽柴靜的《看見》。聽得又哭又笑,象傻了一樣。 開始是下在微信讀書里,想的是空閑的...
    真冉閱讀 114評(píng)論 0 0