1. 程式人生 > >GCD教程(一):基本概念

GCD教程(一):基本概念

什麼是GCD?

Grand Central Dispatch或者GCD,是一套低層API,提供了一種新的方法來進行併發程式編寫。從基本功能上講,GCD有點像NSOperationQueue,他們都允許程式將任務切分為多個單一任務然後提交至工作佇列來併發地或者序列地執行。GCD比之NSOpertionQueue更底層更高效,並且它不是Cocoa框架的一部分。

除了程式碼的平行執行能力,GCD還提供高度整合的事件控制系統。可以設定控制代碼來響應檔案描述符、mach ports(Mach port 用於 OS X上的程序間通訊)、程序、計時器、訊號、使用者生成事件。這些控制代碼通過GCD來併發執行。

GCD的API很大程度上基於block,當然,GCD也可以脫離block來使用,比如使用傳統c機制提供函式指標和上下文指標。實踐證明,當配合block使用時,GCD非常簡單易用且能發揮其最大能力。

你可以在Mac上敲命令“man dispatch”來獲取GCD的文件。

為何使用?

GCD提供很多超越傳統多執行緒程式設計的優勢:

  1. 易用: GCD比之thread跟簡單易用。由於GCD基於work unit而非像thread那樣基於運算,所以GCD可以控制諸如等待任務結束監視檔案描述符週期執行程式碼以及工作掛起等任務。基於block的血統導致它能極為簡單得在不同程式碼作用域之間傳遞上下文。
  2. 效率: GCD被實現得如此輕量和優雅,使得它在很多地方比之專門建立消耗資源的執行緒更實用且快速。這關係到易用性:導致GCD易用的原因有一部分在於你可以不用擔心太多的效率問題而僅僅使用它就行了。
  3. 效能: GCD自動根據系統負載來增減執行緒數量,這就減少了上下午切換以及增加了計算效率。

Dispatch Objects

儘管GCD是純c語言的,但它被組建成面向物件的風格。GCD物件被稱為dispatch object。Dispatch object像Cocoa物件一樣是引用計數的。使用dispatch_release和dispatch_retain函式來操作dispatch object的引用計數來進行記憶體管理。但主意不像Cocoa物件,dispatch object並不參與垃圾回收系統,所以即使開啟了GC,你也必須手動管理GCD物件的記憶體。

Dispatch queues 和 dispatch sources(後面會介紹到)可以被掛起和恢復,可以有一個相關聯的任意上下文指標,可以有一個相關聯的任務完成觸發函式。可以查閱“man dispatch_object”來獲取這些功能的更多資訊。

Dispatch Queues

GCD的基本概念就是dispatch queue。dispatch queue是一個物件,它可以接受任務,並將任務以先到先執行的順序來執行。dispatch queue可以是併發的或序列的。併發任務會像NSOperationQueue那樣基於系統負載來合適地併發進行,序列佇列同一時間只執行單一任務。

GCD中有三種佇列型別:

  1. The main queue: 與主執行緒功能相同。實際上,提交至main queue的任務會在主執行緒中執行。main queue可以呼叫dispatch_get_main_queue()來獲得。因為main queue是與主執行緒相關的,所以這是一個序列佇列。
  2. Global queues: 全域性佇列是併發佇列,並由整個程序共享。程序中存在三個全域性佇列:高、中(預設)、低三個優先順序佇列。可以呼叫dispatch_get_global_queue函式傳入優先順序來訪問佇列。
  3. 使用者佇列: 使用者佇列 (GCD並不這樣稱呼這種佇列, 但是沒有一個特定的名字來形容這種佇列,所以我們稱其為使用者佇列) 是用函式 dispatch_queue_create 建立的佇列. 這些佇列是序列的。正因為如此,它們可以用來完成同步機制, 有點像傳統執行緒中的mutex。

建立佇列

要使用使用者佇列,我們首先得建立一個。呼叫函式dispatch_queue_create就行了。函式的第一個引數是一個標籤,這純是為了debug。Apple建議我們使用倒置域名來命名佇列,比如“com.dreamingwish.subsystem.task”。這些名字會在崩潰日誌中被顯示出來,也可以被偵錯程式呼叫,這在除錯中會很有用。第二個引數目前還不支援,傳入NULL就行了。

提交 Job

向一個佇列提交Job很簡單:呼叫dispatch_async函式,傳入一個佇列和一個block。佇列會在輪到這個block執行時執行這個block的程式碼。下面的例子是一個在後臺執行一個巨長的任務:

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self goDoSomethingLongAndInvolved];
        NSLog(@"Done doing something long and involved");
});

dispatch_async 函式會立即返回, block會在後臺非同步執行。 

當然,通常,任務完成時簡單地NSLog個訊息不是個事兒。在典型的Cocoa程式中,你很有可能希望在任務完成時更新介面,這就意味著需要在主執行緒中執行一些程式碼。你可以簡單地完成這個任務——使用巢狀的dispatch,在外層中執行後臺任務,在內層中將任務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"];
        });
});

還有一個函式叫dispatch_sync,它乾的事兒和dispatch_async相同,但是它會等待block中的程式碼執行完成並返回。結合 __block型別修飾符,可以用來從執行中的block獲取一個值。例如,你可能有一段程式碼在後臺執行,而它需要從介面控制層獲取一個值。那麼你可以使用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

我們還可以使用更好的方法來完成這件事——使用更“非同步”的風格。不同於取介面層的值時要阻塞後臺執行緒,你可以使用巢狀的block來中止後臺執行緒,然後從主執行緒中獲取值,然後再將後期處理提交至後臺執行緒:

    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可以是使用者佇列也可以使全域性佇列。

不再使用鎖(Lock)

使用者佇列可以用於替代鎖來完成同步機制。在傳統多執行緒程式設計中,你可能有一個物件要被多個執行緒使用,你需要一個鎖來保護這個物件:

    NSLock *lock;

訪問程式碼會像這樣:

    - (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來替代:

    dispatch_queue_t queue;

要用於同步機制,queue必須是一個使用者佇列,而非全域性佇列,所以使用usingdispatch_queue_create初始化一個。然後可以用dispatch_async 或者 dispatch_sync將共享資料的訪問程式碼封裝起來:

    - (id)something
    {
        __block id 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是非常輕量級的,所以你可以大用特用,就像你以前使用lock一樣。

現在你可能要問:“這樣很好,但是有意思嗎?我就是換了點程式碼辦到了同一件事兒。”

實際上,使用GCD途徑有幾個好處:

  1. 平行計算: 注意在第二個版本的程式碼中, -setSomething:是怎麼使用dispatch_async的。呼叫 -setSomething:會立即返回,然後這一大堆工作會在後臺執行。如果updateSomethingCaches是一個很費時費力的任務,且呼叫者將要進行一項處理器高負荷任務,那麼這樣做會很棒。
  2. 安全: 使用GCD,我們就不可能意外寫出具有不成對Lock的程式碼。在常規Lock程式碼中,我們很可能在解鎖之前讓程式碼返回了。使用GCD,佇列通常持續執行,你必將歸還控制權。
  3. 控制: 使用GCD我們可以掛起和恢復dispatch queue,而這是基於鎖的方法所不能實現的。我們還可以將一個使用者佇列指向另一個dspatch queue,使得這個使用者佇列繼承那個dispatch queue的屬性。使用這種方法,佇列的優先順序可以被調整——通過將該佇列指向一個不同的全域性佇列,若有必要的話,這個佇列甚至可以被用來在主執行緒上執行程式碼。
  4. 整合: GCD的事件系統與dispatch queue相整合。物件需要使用的任何事件或者計時器都可以從該物件的佇列中指向,使得這些控制代碼可以自動在該佇列上執行,從而使得控制代碼可以與物件自動同步。

總結

現在你已經知道了GCD的基本概念、怎樣建立dispatch queue、怎樣提交Job至dispatch queue以及怎樣將佇列用作執行緒同步。接下來我會向你展示如何使用GCD來編寫平行執行程式碼來充分利用多核系統的效能^ ^。我還會討論GCD更深層的東西,包括事件系統和queue targeting。