天天看點

Objc的底層并發API Objc的底層并發API

Objc的底層并發API

HOME ABOUT GUESTBOOK CATEGORIES TAGS LINKS SUBSCRIBE

本文由webfrogs譯自objc.io,原文作者Daniel Eggert。轉載請注明出處!

小引

本篇英文原文所釋出的站點objc.io是一個專門為iOS和OS X開發者提供的深入讨論技術的平台,文章含金量很高。這個平台每月釋出一次,每次都會有數篇文章針對同一個特殊的主題的不同方面來深入讨論。本月的主題是“并發程式設計”,本文翻譯的正是其中的第4篇文章。

翻譯此文是受到了破船的啟發。他已經将objc.io本月主題的第二篇文章翻譯完成了。

《OC中并發程式設計的相關API和面臨的挑戰(1)》 

《OC中并發程式設計的相關API和面臨的挑戰(2)》

首次翻譯文章,水準有限,歡迎指正。

目錄

1、從前。。。 

2、延後執行 

3、隊列 

    3.1、标記隊列 

    3.2、優先級 

4、孤立隊列 

    4.1、資源保護 

    4.2、單一資源的多讀單寫 

    4.3、鎖競争 

    4.4、全都使用異步分發 

    4.5、如何寫出好的異步API 

5、疊代執行 

6、組 

    6.1、對現有API使用dispatch_group_t 

7、事件源 

    7.1、監視程序 

    7.2、監視檔案 

    7.3、定時器 

    7.4、取消 

8、輸入輸出 

    8.1、GCD和緩沖區 

    8.2、讀和寫 

9、基準測試 

10、原子操作 

    10.1、計數器 

    10.2、比較和交換 

    10.3、原子隊列 

    10.4、自旋鎖

正文

這篇文章裡,我們将會讨論一些iOS和OS X都可以使用的底層API。除了dispatch_once,我們一般不鼓勵使用其中的任何一種技術。

但是我們想要揭示出表面之下深層次的一些可利用的方面。這些底層的API提供了大量的靈活性,但是伴随着靈活性而來的卻是程式複雜度的提升和我們對代碼的更多責任。在我們的文章《common background practices》中提到的高層次的API和模式能夠讓你專注于手頭的任務并且免于大量的問題。并且通常來說,高層次的API會提供更好的性能,除非你能負擔的起使用底層API帶來的糾結和調試代碼的時間和努力。

盡管如此,了解深層次下的軟體堆棧工作原理還是有很有幫助的。我們希望這篇文章能夠讓你更好的了解這個平台,同時,讓你更加感謝這些高層的API。

首先,我們将會分析大多數組成Grand Central Dispatch的部分。數年間,蘋果公司持續添加功能并且改善它。現在蘋果已經将其開源,這意味着它對其他平台也是可用的了。最後,我們将會看一下原子操作——另外的一種底層建構代碼的集合。

也許,關于并發程式設計最好的書是M. Ben-Ari寫的《Principles of Concurrent Programming》ISBN 0-13-701078-8。如果你正在做任何有關并發程式設計的事情,你需要讀一下這本書。這本書已經寫了超過30年了,但仍然是無法超越。簡潔的寫法,優秀的例子和練習,帶領你建構并發程式設計中的基本代碼塊。這本書現在已經絕版了,但是仍然有一些零散的影印本。有一個新版書,名字叫《Principles of Concurrent and Distributed Programming》ISBN 0-321-31283-X,好像有很多相同的地方,不過我還沒有讀過。

1、從前。。。

也許GCD中使用最多并且被濫用的就是dispatch_once了。正确的用法看起來是這樣的:

+ (UIColor *)boringColor;
{
    static UIColor *color;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        color = [UIColor colorWithRed:0.380f green:0.376f blue:0.376f alpha:1.000f];
    });
    return color;
}
           

這段代碼僅僅隻會運作一次。并且在連續調用這段代碼的期間,檢查操作是很高效的。你能使用它來初始化全局的資料比如單例。要注意到的是,使用dispatch_once_t會使得測試變得非常困難(單例和測試隻能任取其一)。

要確定onceToken被聲明為static,或者有全局作用域。任何其他的情況都會導緻無法預知的行為。換句話說,不要把dispatch_once_t作為一個對象的成員變量,或者類似的情形。

退回到遠古時代(其實也就是幾年前),人們會使用pthread_once,因為dispatch_once_t更容易使用并且不易出錯,是以你永遠都不會使用到pthread_once了。

2、延後執行

另一個常見的朋友就是dispatch_after了。它使工作延後執行。它是很強大的,但是要注意:你很容易就陷入到一堆麻煩中。一般的使用是這樣的:

- (void)foo
{
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self bar];
    });
}
           

咋一看,這段代碼是極好的。但是這裡也存在一些缺點。我們不能(直接)取消我們已經送出到dispatch_after的代碼,它将會運作。

另外一個需要注意的事情就是,在人們使用dispatch_after去完成工作時,容易寫出有時間bug的問題傾向的代碼。舉例來說,一些代碼運作的太早并且很可能不知道為什麼,這時你把它放到了dispatch_after中,現在,所有代碼運作正常了。但是,幾周以後,程式停止工作了,并且由于你沒有明确指出代碼是以什麼樣的次序運作的,調試代碼就變成了一場噩夢。永遠不要這樣做,大多數的情況下,你最好把代碼放到正确的位置。如果代碼放到 -viewWillAppear 太早,那麼或許 -viewDidAppear 就是正确的地方。

你将會為自己省去很多麻煩,通過在自己代碼中建立直接調用(類似-viewDidAppear)而不是依賴于 dispatch_after。

如果你需要一些事情在某個特定的時刻及時的運作,那麼 dispatch_after 或許會是個好的選擇。確定同時考慮了NSTimer,這個API雖然有點笨重,但是它允許你取消這個定時。

3、隊列

GCD的一個最基本的部分就是隊列。下面我們會給出一些如何使用它的例子。當使用隊列的時候,給它們一個好的标簽會幫自己不少忙。當調試的時候,這個标簽會在Xcode(和lldb)中顯示,這會幫助你了解應用程式目前是由誰負責的:

- (id)init;
{
    self = [super init];
    if (self != nil) {
        NSString *label = [NSString stringWithFormat:@"%@.isolation.%p", [self class], self];
        self.isolationQueue = dispatch_queue_create([label UTF8String], 0);
        
        label = [NSString stringWithFormat:@"%@.work.%p", [self class], self];
        self.workQueue = dispatch_queue_create([label UTF8String], 0);
    }
    return self;
}
           

隊列可以是并行也可以是串行的。預設情況下,他們是串行的,也就是說,任何給定的時間内,隻能有一個單獨的代碼塊運作。這就是孤立隊列的運作方式。隊列也可以是并行的,也就是同一時間内允許多代碼塊同時執行。

GCD隊列的内部使用的是線程。GCD管理這些線程,并且使用GCD的時候,你不需要自己建立線程。重要的外在部分是GCD提供給你的使用者API,一個不同的抽象層級的接口。當使用GCD來完成并發的工作時,你不必考慮線程方面的問題,代替的,隻需考慮隊列和工作項目(送出給隊列的代碼)。當然在這些之下的,依然是線程在工作。GCD的抽象層次為你通常的代碼編寫提供了更好的方式。

隊列和工作項目的方式同時解決了一個普遍的會輻射出去的并發問題:如果我們直接使用線程,并且想要做一些并發的事情,我們可能将我們的工作分成100個小的工作項目,同時基于可以利用的CPU核心數量來建立線程,姑且是8線程。我們把這些工作項目送到這8個線程中。但是寫這個函數的人同時也想要使用并發,是以當你調用這個函數的時候,也會建立8個線程。現在,你有了 8×8=64 個線程,盡管你隻有8個CPU核心,也就是說任何時候隻有12%的線程能夠運作這時另外88%的線程什麼事情都沒做。使用GCD,你不會有這種問題,當作業系統關閉CPU核心以省電時,GCD甚至能夠對應的調整線程數量。

GCD通過建立所謂的線程池來大緻比對CPU核心數量。要記住,線程的建立并不是無代價的。每個線程都需要占用記憶體和核心資源。這裡也有一個問題:如果你送出了一個代碼塊給GCD,但是這個代碼塊阻塞了這個線程,那麼這個線程在這段時間内就不是可用的,并且不能及時處理其他工作——它被阻塞了。為了確定工作項目在隊列上一直是執行的,GCD不得不建立一個新的線程,并将新線程添加到線程池。

如果你的代碼正在阻塞許多線程,這回帶來很大的問題。最開始,線程消耗資源,更多的時,建立他們會變得代價高昂。這需要時間,而且在這段時間内,GCD無法以全速來運作工作項目。有許多能夠導緻線程阻塞的事情,但是最常見的時與I/O操作有關,也就是從檔案或者網絡中讀寫資料。正是因為這些原因,你不應該在GCD隊列中以阻塞的方式來運作I/O操作。看一下下面的輸入輸出段落以了解如何以GCD良好運作的方式來進行I/O操作。

3.1、标記隊列

你能夠為你建立的任何一個隊列設定标記。這會是很強大的,并且有助于調試。

為每一個類建立自己的隊列而不是使用全局的隊列被認為是一種好的方式。這種放肆下,你可以設定隊列的名字,這讓調試變得輕松許多——Xcode可以讓你在Debug Navigator中看到所有的隊列名字,或者你可以直接使用lldb。(lldb) thread list指令将會在控制台列印出所有隊列的名字。一旦你使用大量的異步内容,這是很有價值的幫助。

使用私有隊列同樣強調封裝性。這時你自己的隊列,你要自己決定如何使用它。

預設情況下,一個新建立的隊列轉發到預設優先級的全局隊列中。我們就将會多讨論一點有關優先級的東西。

你可以改變你隊列轉發到的隊列——也就是說你可以設定自己隊列的目标隊列。以這種方式,你可以将不同隊列連結在一起。你的類Foo的隊列轉發到類Bar的隊列,而類Bar的隊列又轉發到全局隊列。

當你使用孤立隊列(之後我們也會讨論)的時候,這會很有用。Foo有一個孤立隊列,并且轉發到Bar的孤立隊列,考慮到Bar的孤立隊列所保護的資源,它會自動變為線程安全的。

如果你希望多段代碼同時運作,那要確定你自己的隊列是并發的。同時需要注意,如果一個隊列的目标隊列使串行的(也就是非并發),那麼實際上這個隊列也會轉換為一個串行隊列。

3.2、優先級

你通過設定目标為全局隊列中的一個來改變自己隊列的優先級,但是你應該克制這麼做的沖動。

在大多數情況下,改變優先級不會使事情照你預想的方向運作。一些看起簡單的事情實際上是一個非常複雜的問題。你很容易會碰到一個叫做優先級反轉的情況。我們的文章《Concurrent Programming: APIs and Challenges》(已經由破船翻譯了,詳見點選此處)有更多關于這個問題的資訊,這個問題幾乎導緻了NASA的探路者火星漫遊器變成磚頭。

在此基礎上,使用DISPATCH_QUEUE_PRIORITY_BACKGROUND隊列時,你需要格外小心。除非你了解了throttled I/O and background status as per setpriority(2) (抱歉,我也沒了解,不知如何翻譯了。)的意思,否則不要使用它。 不然,系統可能會以難以忍受的方式終止你的應用程式的運作。這可能集中在處理I/O操作上,這種操作以一種不與系統其他處理I/O操作的部分互動的方式運作。但是和優先級反轉結合起來,這回變成一種危險的情況。

4、孤立隊列

孤立隊列是GCD隊列使用中非常普遍的一種模式。這裡有兩個變種。

4.1、資源保護

多線程程式設計中,最常見的情形是你有一個資源,每次隻有一個線程被允許通路這個資源。

我們在《有關并發程式設計的文章》(參考破船的譯文)中讨論了資源在并發程式設計中意味着什麼,其實并發程式設計中的資源通常就是一塊記憶體或者一個對象,每次隻有一個線程可以通路它。

舉例來說,我們需要以多線程(或者多個隊列)方式通路NSMutableDictionary。我們可能會照下面的代碼來做:

- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
    key = [key copy];
    dispatch_async(self.isolationQueue, ^(){
        if (count == 0) {
            [self.counts removeObjectForKey:key];
        } else {
            self.counts[key] = @(count);
        }
    });
}

- (NSUInteger)countForKey:(NSString *)key;
{
    __block NSUInteger count;
    dispatch_sync(self.isolationQueue, ^(){
        NSNumber *n = self.counts[key];
        count = [n unsignedIntegerValue];
    });
    return count;
}
           

通過以上代碼,僅僅隻有一個線程可以通路NSMutableDictionary的執行個體。

注意以下四點:

  1. 不要使用上面的代碼,請先閱讀多讀單寫和鎖競争
  2. 我們使用async當存儲一個值的時候,這很重要。我們不想,也不必阻塞目前線程去等待寫操作完成。當讀操作時,我們使用sync因為我們需要傳回值。
  3. 根據函數的定義,-setCount:forKey:需要一個NSString值,我們是使用dispatch_async來傳遞該值。在代碼執行之前,函數的調用者可以自由傳遞一個NSMutableString值并且在函數傳回後可以修改它。是以我們必須對傳入的字元串使用copy操作以確定函數能夠正确地工作。如果傳入的字元串不是可變的(也就是正常的NSString類型),調用copy基本上是個空操作。
  4. isolationQueue建立時,參數dispatch_queue_attr_t的值需要是DISPATCH_QUEUE_SERIAL(或者0)。

4.2、單一資源的多讀單寫

我們能夠改善上面的那個例子。GCD有可以讓多線程運作的并發隊列。我們能夠安全地使用多線程來從NSMutableDictionary中讀取隻要我們不同時修改它。當我們需要改變這個字典時,我們使用barrier來分發這個塊代碼。這樣的塊代碼會在所有之前預定好的塊代碼完成之後執行,并且所有在它之後的塊都會在它完成後才會執行。

我們以以下方式建立隊列:

self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);
           

并且用以下代碼來改變setter函數:

- (void)setCount:(NSUInteger)count forKey:(NSString *)key
{
    key = [key copy];
    dispatch_barrier_async(self.isolationQueue, ^(){
        if (count == 0) {
            [self.counts removeObjectForKey:key];
        } else {
            self.counts[key] = @(count);
        }
    });
}
           

當你使用并發隊列時,要確定所有的barrier調用都是async(異步)的。如果你使用dispatch_barrier_sync,那麼你很可能會使你自己(更确切的說是,你的代碼)産生死鎖。寫操作需要barrier,并且可以是async的。

4.3、鎖競争

首先,這裡有一句警告:上面這個例子中我們保護的資源是一個NSMutableDictionary,這段代碼作為一個例子運作的不錯。但是在真實的代碼環境下,把孤立隊列放到一個正确的複雜度層級下是很重要的。

如果你對NSMutableDictionary的通路操作變得非常頻繁,你會碰到一個已知的叫做鎖競争的問題。鎖競争并不是隻是在GCD和隊列下才變得特殊,任何使用了鎖機制的程式都會碰到同樣的問題——隻不過不同的鎖機制會以不同的方式碰到。

所有對dispatch_async,dispatch_sync等等的調用都需要完成某種形式的鎖——以確定僅有一個線程或者特定的線程運作所給的塊代碼。GCD在一些範圍可以避免使用鎖而以時序安排來代替,但在最後,問題隻是指有所變化。根本問題仍然存在:如果你有大量的線程在同一時間去競争同一個鎖,你就會看到性能的變化,性能會嚴重下降。

你應該從直接複雜層次中隔離開。當你發現了性能下降,這是表明代碼中,存在明顯的設計問題。這裡有兩個地方的開銷需要你來平衡。第一個是獨占臨界區資源太久的開銷,以至于别的線程都從進入臨界區的操作中阻塞。第二個是太頻繁進出臨界區的開銷。在GCD的世界裡,第一種開銷的情況就是一個塊代碼在孤立隊列中運作,它可能潛在的阻塞了其他将要在這個孤立隊列中運作的代碼。第二種開銷對應的就是調用dispatch_async和dispatch_sync的開銷。無論再怎麼優化,這兩個動作都不是無代價的。

不幸的是,不存在通用的标準來說明什麼是正确的平衡,你需要自己評測和調整。

如果你看上面例子中的代碼,我們的臨界區代碼僅僅做了很簡單的事情。這可能也可能不是好的,依賴于它怎麼被使用。

在你自己的代碼中,要考慮自己是否在更高的層次保護了孤立隊列。舉個例子,類Foo有一個孤立隊列并且它本身保護着自己通路NSMutableDictionary,有可能有一個用到了Foo的類Bar有一個孤立隊列保護所有對類Foo的使用。換句話說,你需要把類Foo改變為不再是線程安全的(沒有孤立隊列),并在Bar中,使用一個孤立隊列來確定同一時間隻能有一個線程使用Foo。

4.4、全都使用異步分發

我們在這稍稍轉變以下話題。正如你在上面看到的,你可以分發一個塊,一個工作單元的方式,即可以是同步的,也可以是異步的。我們在關于并發API和陷阱的文章(可以參考破船的譯文,見本文開頭)中讨論最多的就是死鎖。在GCD中,以同步分發的方式非常容易出現這種情況。見下面的代碼:

dispatch_queue_t queueA; // assume we have this
dispatch_sync(queueA, ^(){
    dispatch_sync(queueA, ^(){
        foo();
    });
});
           

一旦我們進入到第二個dispatch_sync,就會發生死鎖。我們不能分發到queueA,因為有人(目前線程)正在隊列中并且永遠不會離開。但是有更隐晦的死鎖方式:

dispatch_queue_t queueA; // assume we have this
dispatch_queue_t queueB; // assume we have this

dispatch_sync(queueA, ^(){
    foo();
});

void foo(void)
{
    dispatch_sync(queueB, ^(){
        bar();
    });
}

void bar(void)
{
    dispatch_sync(queueA, ^(){
        baz();
    });
}
           

單獨的每次調用dispatch_sync()看起來都沒有問題,但是一旦組合起來,就會發生死鎖。

這是使用同步分發存在的固有問題,如果我們使用異步分發,比如:

dispatch_queue_t queueA; // assume we have this
dispatch_async(queueA, ^(){
    dispatch_async(queueA, ^(){
        foo();
    });
});
           

一切運作正常。異步調用不會産生死鎖。是以值得我們在任何可能的時候都使用異步分發。我們使用一個異步調用結果塊的函數,來代替編寫一個傳回值(這必須要用同步)的方法或者函數。這種方式,我們會有更少發生死鎖的可能性。

異步調用的副作用就是它們很難調試。當我們停止了調試器中的代碼,再回溯并檢視已經變得沒有意義了。

要記住這些。死鎖通常是最難處理的問題。

4.5、如何寫出好的異步API

如果你正在給設計一個給别人(或者是給自己)使用的API,你需要記住幾個好的實踐。

正如我們剛剛提到的,你需要傾向于異步API。當你建立一個API,它會在你的控制之外以各種方式調用,如果你的代碼能産生死鎖,那麼死鎖就會發生。

如果你需要寫的函數或者方法,那麼讓它們調用dispatch_async()。不要讓你的函數調用者來這麼做,調用者應該可以通過調用你提供的方法或者函數來做到這個。

如果你的方法或函數有一個傳回值,通過一個回調的處理來異步傳遞傳回值。這個API應該是這樣的,你的方法或函數持有一個結果塊代碼和一個将結果傳遞到的目标隊列。你函數的調用着不需要自己來将結果分發。這麼做的原因很簡單:幾乎所有時間,調用者都需要在一個适當的隊列中,這種方式的代碼是很容易被閱讀的。并且你的函數無論如何将會(必須)調用dispatch_async()來進行回調處理。

如果你寫一個類,讓你類的使用這設定一個将回調傳遞到的隊列會是一個好的選擇。你的代碼可能像這樣:

- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler;
{
    dispatch_async(self.isolationQueue, ^(void){
        // do actual processing here
        dispatch_async(self.resultQueue, ^(void){
            handler(YES);
        });
    });
}
           

如果你以這種方式來寫你的類,讓類一起工作就會變得相當容易。如果類A使用了類B,它會把自己的孤立隊列設定為B的回調隊列。

5、疊代執行

如果你正在擺弄一些數字,并且手頭上的問題可以拆分為小的同樣的部分,那麼dispatch_apply會很有用。

如果你的代碼看起來是這樣的:

for (size_t y = 0; y < height; ++y) {
    for (size_t x = 0; x < width; ++x) {
        // Do something with x and y here
    }
}
           

小小的改動,你或許可以就可以讓他運作的更快:

dispatch_apply(height, dispatch_get_global_queue(0, 0), ^(size_t y) {
    for (size_t x = 0; x < width; x += 2) {
        // Do something with x and y here
    }
});
           

代碼運作更好的程度取決于你在循環内部做的操作。

block中運作的工作一定要是非常重要的,否則最外層的那個dispatch_apply的定義就顯得太繁瑣了。

除非代碼受到計算帶寬的限制,每個工作單元讀寫所需要的合适的緩存大小所占用記憶體是無關緊要的,這對性能會帶來顯著的影響。受到臨界區限制的代碼可能不會運作良好。詳細讨論這些問題已經超出了這篇文章的範圍。使用dispatch_apply可能會對性能提升有所幫助,但是性能優化本身是個很複雜的主題。維基百科上有一篇關于Memory-bound function的文章。記憶體通路速度在L2,L3和主存上變化很大。當你的資料通路模式與緩存大小不比對時,10倍的性能下降的情況并不少見。

6、組

很多時候,你發現需要将幾個異步代碼塊組合起來去完成一個給定的任務。這些任務中甚至有些是可以并行的。現在,如果你想要在這些代碼塊都執行完成後運作一些代碼,“組”可以完成這項任務。看這裡的例子:

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^(){
    // Do something that takes a while
    [self doSomeFoo];
    dispatch_group_async(group, dispatch_get_main_queue(), ^(){
        self.foo = 42;
    });
});
dispatch_group_async(group, ^(){
    // Do something else that takes a while
    [self doSomeBar];
    dispatch_group_async(group, dispatch_get_main_queue(), ^(){
        self.bar = 1;
    });
});

// This block will run once everything above is done:
dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
    NSLog(@"foo: %d", self.foo);
    NSLog(@"bar: %d", self.bar);
});
           

需要注意到的重要的事情是,所有的這些都是非阻塞的。我們從未讓目前的線程一直等待直到别的任務做完。恰恰相反,我們隻是簡單的将多個代碼塊放入隊列。由于代碼不會阻塞,是以就不會産生死鎖。

同時需要注意的是,在這個小的簡單的例子中,我們是怎麼在不同的隊列間進切換的。

6.1、對現有API使用dispatch_group_t

一旦你将組作為你的工具箱中的一部分,你可能會想知道為什麼大多數的異步API不把dispatch_group_t作為其的一個可選參數。這沒有什麼令人絕望的理由,僅僅是因為自己添加這個功能太簡單了,但是你還是要小心以確定自己的代碼是成對出現的。

舉例來說,我們可以給Core Data的-performBlock:函數添加上組的功能,那麼API會變得像這個樣子:

- (void)withGroup:(dispatch_group_t)group performBlock:(dispatch_block_t)block
{
    if (group == NULL) {
        [self performBlock:block];
    } else {
        dispatch_group_enter(group);
        [self performBlock:^(){
            block();
            dispatch_group_leave(group);
        }];
    }
}
           

這樣做允許我們使用dispatch_group_notify來運作一段代碼,當Core Data上的一堆操作完成以後。

很明顯,我們可以給NSURLConnection做同樣的事情:

+ (void)withGroup:(dispatch_group_t)group 
        sendAsynchronousRequest:(NSURLRequest *)request 
        queue:(NSOperationQueue *)queue 
        completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
{
    if (group == NULL) {
        [self sendAsynchronousRequest:request 
                                queue:queue 
                    completionHandler:handler];
    } else {
        dispatch_group_enter(group);
        [self sendAsynchronousRequest:request 
                                queue:queue 
                    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
            handler(response, data, error);
            dispatch_group_leave(group);
        }];
    }
}
           

為了能正常工作,你需要確定:

  • dispatch_group_enter()一定要在dispatch_group_leave()之前運作。
  • dispatch_group_enter()和dispatch_group_leave()通常是成對出現的(就算有錯誤産生時)。

7、事件源

GCD有一個較少人知道的特性:事件源dispatch_source_t。

正如大多數的GCD,它也是很底層的。當你需要用到它時,它會變得極其有用。它的一些使用是秘傳招數,我們将會接觸到一部分。是事件源大部分對iOS平台來說不是很有用,因為在iOS平台有諸多限制,你無法啟動程序(是以就沒有必要監視程序),也不能在你的app之外寫資料(是以也就沒有必要去監視檔案)等等。

GCD事件源是以極其資源高效的方式實作的。

7.1、監視程序

如果一些程序正在運作而你想知道他們什麼時候存在,GCD能夠做到這些。你也可以使用GCD來檢測程序什麼時候分叉,也就是産生了子程序或者一個信号被傳送給了程序(比如SIGTERM)。

NSRunningApplication *mail = [NSRunningApplication 
  runningApplicationsWithBundleIdentifier:@"com.apple.mail"];
if (mail == nil) {
    return;
}
pid_t const pid = mail.processIdentifier;
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, 
  DISPATCH_PROC_EXIT, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(self.source, ^(){
    NSLog(@"Mail quit.");
});
dispatch_resume(self.source);
           

當Mail.app退出的時候,這個程式會列印出Mail quit.。

7.2、監視檔案

這種可能性是無窮盡的。你能直接監視一個檔案的改變,并且當改變發生時,事件源的事件處理将會被調用。

你也可以使用它來監視檔案夾,比如建立一個watch folder。

NSURL *directoryURL; // assume this is set to a directory
int const fd = open([[directoryURL path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
    char buffer[80];
    strerror_r(errno, buffer, sizeof(buffer));
    NSLog(@"Unable to open \"%@\": %s (%d)", [directoryURL path], buffer, errno);
    return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, 
  DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
    unsigned long const data = dispatch_source_get_data(source);
    if (data & DISPATCH_VNODE_WRITE) {
        NSLog(@"The directory changed.");
    }
    if (data & DISPATCH_VNODE_DELETE) {
        NSLog(@"The directory has been deleted.");
    }
});
dispatch_source_set_cancel_handler(source, ^(){
    close(fd);
});
self.source = source;
dispatch_resume(self.source);
           

你應該一直添加DISPATCH_VNODE_DELETE去檢測檔案或者檔案夾是否已經被删除——然後就停止監聽。

7.3、定時器

大多數情況下,對于定時事件,你會選擇NSTimer。定時器的GCD版本是底層的,它會給你更多控制權——但要小心使用。

需要特别重點指出的是,為了讓OS節省電量,需要為GCD的定時器接口指定一個低的誤內插補點。如果你不必要的指定了一個過低的誤內插補點,你将會浪費更多的電量。

這裡我們設定了一個5秒的定時器,并允許有十分之一秒的誤差:

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 
  0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
    NSLog(@"Time flies.");
});
dispatch_time_t start
dispatch_source_set_timer(source, DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC, 
  100ull * NSEC_PER_MSEC);
self.source = source;
dispatch_resume(self.source);
           

7.4、取消

所有的事件源都允許你添加一個cancel handler。這對清理你為事件源建立的任何資源都是很有幫助的,比如關閉檔案描述符。GCD保證在cancel handle調用前,所有的事件處理都已經完成調用。

看上面的監視檔案例子中對dispatch_source_set_cancel_handler()的使用。

8、輸入輸出

寫出能夠在繁重的I/O處理情況下運作良好的代碼是一件非常棘手的事情。GCD有一些能夠幫上忙的地方。不會涉及太多的細節,我們隻簡單的分析下問題是什麼,GCD是怎麼處理的。

習慣上,當你從一個網絡套接字中讀取資料時,你要麼做一個阻塞的讀取操作,也就是讓你個線程一直等待直到資料變得可用,或者是做反複的輪詢操作。這兩種方法都是很浪費資源并且無法度量。然而,kqueue解決了輪詢的問題,通過當資料變得可用時傳遞一個事件,GCD也采用了同樣的方法,但是更加優雅。當向套接字寫資料時,同樣的問題也存在,這時你要麼做阻塞的寫操作,要麼等待套接字能夠接收資料。

在處理I/O時,還有一個問題就是資料是以塊的形式到達的。當從網絡中讀取資料時,依據MTU(最大傳輸單元)資料塊典型的大小是在1.5K左右。這使得資料塊内可以是任何内容。一旦資料到達,你通常隻是對跨多個資料塊的内容感興趣。而且通常你會在一個大的緩沖區裡将資料組合起來然後再進行處理。假設(人為例子)你收到了這樣8個資料塊:

0: HTTP/1.1 200 OK\r\nDate: Mon, 23 May 2005 22:38
1: :34 GMT\r\nServer: Apache/1.3.3.7 (Unix) (Red-H
2: at/Linux)\r\nLast-Modified: Wed, 08 Jan 2003 23
3: :11:55 GMT\r\nEtag: "3f80f-1b6-3e1cb03b"\r\nCon
4: tent-Type: text/html; charset=UTF-8\r\nContent-
5: Length: 131\r\nConnection: close\r\n\r\n<html>\r
6: \n<head>\r\n  <title>An Example Page</title>\r\n
7: </head>\r\n<body>\r\n  Hello World, this is a ve
           

如果你是在尋找HTTP的頭部,将所有資料塊組合成一個大的緩沖區并且從中查找\r\n\r\n是非常簡單的。但是這樣做,你會大量地複制這些資料。大量舊的C語言API存在的一個問題就是,緩沖區沒有所有權的概念,是以函數不得不将資料再次拷貝到自己的緩沖區中——又一次的拷貝。拷貝資料操作看起來是無關緊要的,但是當你正在做大量的I/O操作的時候,你會在你的profiling tool(Instruments)中看到這些拷貝操作大量出現。即使你僅僅每個記憶體區域拷貝一次,你還是使用了兩倍的存儲帶寬并且占用了兩倍的記憶體緩存。

8.1、GCD和緩沖區

最直接了當的方法是使用資料緩沖區。GCD有一個dispatch_data_t類型,在某種程度上和Objective-C的NSData類型很相似。但是它能做别的事情,而且更通用。

注意,dispatch_data_t能夠做retain和release操作,并且dispatch_data_t擁有它持有的對象。

這看起來無關緊要,但是我們必須記住GCD隻是一個普通的C API,并且不能使用Objective-C。 通常的做法是建立一個緩沖區,這個緩沖區要麼是基于棧的,要麼是malloc操作配置設定的記憶體區域,這些都沒有所有權。

dispatch_data_t的一個獨特的屬性是它可以基于零碎的記憶體區域。這解決了我們剛提到的組合記憶體的問題。當你要将兩個資料對象連接配接起來時:

dispatch_data_t a; // Assume this hold some valid data
dispatch_data_t b; // Assume this hold some valid data
dispatch_data_t c = dispatch_data_create_concat(a, b);
           

資料對象c并不會将a和b拷貝到一個單獨的,更大的記憶體區域裡去。相反,它隻是簡單地持有a和b。你可以使用dispatch_data_apply來周遊對象c持有的記憶體區域:

dispatch_data_apply(c, ^(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
    fprintf(stderr, "region with offset %zu, size %zu\n", offset, size);
    return true;
});
           

類似的,你可以使用dispatch_data_create_subrange來建立一個不做任何拷貝操作的子區域。

8.2、讀和寫

在GCD的核心中,Dispatch I/O就是所謂的通道。排程I/O通道提供了一種從檔案描述符中讀寫的不同的方式。建立這樣一個通道最基本的方式就是調用:

dispatch_io_t dispatch_io_create(dispatch_io_type_t type, dispatch_fd_t fd, 
  dispatch_queue_t queue, void (^cleanup_handler)(int error));
           

這将傳回一個持有檔案描述符的建立好的通道。在你通過它建立了通道之後,你不準以任何方式修改這個檔案描述符。

有兩種從根本上不同類型的通道:流和随機存取。如果你打來了硬碟上的一個檔案,你可以使用它來建立一個随機存取的通道(因為這樣的檔案描述符是可尋址的)。如果你打開了一個套接字,你可以建立一個流通道。

如果你想要為一個檔案建立一個通道,你最好使用需要一個路徑參數的dispatch_io_create_with_path,并且讓GCD來打開這個檔案。這時有利的,因為GCD能夠延遲打開這個檔案,以限制同一時間同時打開的檔案數量。

類似通常的read(2),write(2)和close(2)的操作,GCD提供了dispatch_io_read,dispatch_io_write和dispatch_io_close。無論何時資料被讀完或者寫完,讀寫操作通過調用一個回調塊來結束。這些都是以非阻塞,異步I/O的形式高效實作的。

在這你得不到所有的細節,但是這裡會提供一個建立TCP服務端的例子:

首先我們建立一個監聽套接字,并且設定一個接收連接配接的事件源:

_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_nativeSocket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in sin = {};
sin.sin_len = sizeof(sin);
sin.sin_family = AF_INET6;
sin.sin_port = htons(port);
sin.sin_addr.s_addr= INADDR_ANY;
int err = bind(result.nativeSocket, (struct sockaddr *) &sin, sizeof(sin));
NSCAssert(0 <= err, @"");

_eventSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _nativeSocket, 0, _isolation);
dispatch_source_set_event_handler(result.eventSource, ^{
    acceptConnection(_nativeSocket);
});
           

當接受了連接配接,我們建立一個I/O通道:

typedef union socketAddress {
    struct sockaddr sa;
    struct sockaddr_in sin;
    struct sockaddr_in6 sin6;
} socketAddressUnion;

socketAddressUnion rsa; // remote socket address
socklen_t len = sizeof(rsa);
int native = accept(nativeSocket, &rsa.sa, &len);
if (native == -1) {
    // Error. Ignore.
    return nil;
}

_remoteAddress = rsa;
_isolation = dispatch_queue_create([[self description] UTF8String], 0);
_channel = dispatch_io_create(DISPATCH_IO_STREAM, native, _isolation, ^(int error) {
    NSLog(@"An error occured while listening on socket: %d", error);
});

//dispatch_io_set_high_water(_channel, 8 * 1024);
dispatch_io_set_low_water(_channel, 1);
dispatch_io_set_interval(_channel, NSEC_PER_MSEC * 10, DISPATCH_IO_STRICT_INTERVAL);

socketAddressUnion lsa; // remote socket address
socklen_t len = sizeof(rsa);
getsockname(native, &lsa.sa, &len);
_localAddress = lsa;
           

如果我們想要設定SO_KEEPALIVE(如果我們使用了HTTP的keep-alive等級),我們需要在調用dispatch_io_create前這麼做。

建立好I/O通道後,我們可以設定讀取處理程式:

dispatch_io_read(_channel, 0, SIZE_MAX, _isolation, ^(bool done, dispatch_data_t data, int error){
    if (data != NULL) {
        if (_data == NULL) {
            _data = data;
        } else {
            _data = dispatch_data_create_concat(_data, data);
        }
        [self processData];
    }
});
           

如果所有你想做的隻是讀取或者寫入一個檔案,GCD提供了兩個友善的包裝器:dispatch_read和dispatch_write。你需要傳遞給dispatch_read一個檔案路徑和一個在所有資料塊讀取完後調用的代碼塊。類似的,dispatch_write需要一個檔案路徑和一個被寫入的dispatch_data_t對象。

9、基準測試

在GCD的一個不起眼的角落,你會發現一個适合優化代碼的靈巧小工具:

uint64_t dispatch_benchmark(size_t count, void (^block)(void));
           

把這個聲明放到你的代碼中,你就能夠測量給定的代碼塊執行的平均的納秒數。例子如下:

size_t const objectCount = 1000;
uint64_t n = dispatch_benchmark(10000, ^{
    @autoreleasepool {
        id obj = @42;
        NSMutableArray *array = [NSMutableArray array];
        for (size_t i = 0; i < objectCount; ++i) {
            [array addObject:obj];
        }
    }
});
NSLog(@"-[NSMutableArray addObject:] : %llu ns", n);
           

在我的機器上輸出了:

-[NSMutableArray addObject:] : 31803 ns
           

也就是說添加1000個對象到NSMutableArray總共消耗了31803納秒,或者說平均一個對象消耗32納秒。

正如dispatch_benchmark的幫助界面指出的,測量性能并非如看起來那樣不重要。尤其是當比較并發代碼和非并發代碼時,你需要注意你的硬體上運作的特定計算帶寬和記憶體帶寬。不同的機器會很不一樣。如果代碼的性能與通路臨界區有關,那麼我們上面提到的鎖競争問題就會有所影響。

不要把它放到釋出代碼中,事實上,這是無意義的,它是私有API。它隻是在調試和性能分析上起作用。

通路幫助界面:

curl "http://opensource.apple.com/source/libdispatch/libdispatch-84.5/man/dispatch_benchmark.3?txt" 
  | /usr/bin/groffer --tty -T utf8
           

10、原子操作

頭檔案libkern/OSAtomic.h裡有許多強大的函數,專門用來底層多線程程式設計。盡管它是核心頭檔案的一部分,它也能夠在核心之外來幫助程式設計。

這些函數都是很底層的,并且你需要知道一些額外的事情。就算你已經知道了,你還可能會發現一兩件你不能做,或者不易做的事情。當你正在為高性能代碼工作或者正在實作無鎖的和無等待的算法工作時,這些函數會吸引你。

這些函數在atomic(3)的幫助頁裡全部有概述——運作man 3 atomic指令以得到完整的文檔。你會發現裡面讨論到了記憶體屏障。檢視維基百科中關于記憶體屏障的文章。如果你不能确定,那麼你很可能需要它。

10.1、計數器

OSAtomicIncrement和OSAtomicDecrement有一個很長的函數清單允許你以原子操作的方式去增加和減少一個整數值,這不必使用鎖(或者隊列)同時也是線程安全的。如果你需要讓一個全局的計數器值增加,而這個計數器為了統計目的而由多個線程操作,使用原子操作是很有幫助的。如果你要做的僅僅是增加一個全局計數器,那麼無屏障版本的OSAtomicIncrement是很合适的,并且當沒有鎖競争時,調用它們的代價很小。

類似的,OSAtomicOr,OSAtomicAnd,OSAtomicXor的函數能用來進行邏輯運算,而OSAtomicTest可以用來設定和清除位。

10.2、比較和交換

OSAtomicCompareAndSwap能用來做無鎖的懶初始化,如下:

void * sharedBuffer(void)
{
    static void * buffer;
    if (buffer == NULL) {
        void * newBuffer = calloc(1, 1024);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, newBuffer, &buffer)) {
            free(newBuffer);
        }
    }
    return buffer;
}
           

如果沒有緩沖區,我們會建立一個然後自動将其寫到buffer中如果buffer為NULL。在極稀少的情況下,其他人因為多線程也同時設定了buffer,我們簡單的将其釋放掉。因為比較和交換方法是原子的,是以它是一個線程安全的方式去懶初始化值。NULL的檢測和設定buffer是以原子方式完成的。

明顯的,使用dispatch_once()我們也可以完成類似的事情。

10.3、原子隊列

OSAtomicEnqueue()和OSAtomicDequeue()可以讓你實作一個LIFO隊列,以線程安全,無鎖的方式。對有潛在精确要求的代碼來說,這會是強大的建構方式。

還有OSAtomicFifoEnqueue()和OSAtomicFifoDequeue()是為了操作FIFO隊列,但這些隻有在頭檔案中才有文檔——使用他們的時候要小心。

10.4、自旋鎖

最後,OSAtomic.h頭檔案定義了使用自旋鎖的函數:OSSpinLock。再次的,維基百科有深入的有關自旋鎖的資訊。使用指令man 3 spinlock檢視幫助頁的spinlock(3)。當沒有鎖競争時使用自旋鎖代價很小。

在合适的情況下,使用自旋鎖對性能優化是很有幫助的。一如既往:先測量,然後優化。不要做樂觀的優化。

下面是OSSpinLock的一個例子:

@interface MyTableViewCell : UITableViewCell

@property (readonly, nonatomic, copy) NSDictionary *amountAttributes;

@end



@implementation MyTableViewCell
{
    NSDictionary *_amountAttributes;
}

- (NSDictionary *)amountAttributes;
{
    if (_amountAttributes == nil) {
        static __weak NSDictionary *cachedAttributes = nil;
        static OSSpinLock lock = OS_SPINLOCK_INIT;
        OSSpinLockLock(&lock);
        _amountAttributes = cachedAttributes;
        if (_amountAttributes == nil) {
            NSMutableDictionary *attributes = [[self subtitleAttributes] mutableCopy];
            attributes[NSFontAttributeName] = [UIFont fontWithName:@"ComicSans" size:36];
            attributes[NSParagraphStyleAttributeName] = [NSParagraphStyle defaultParagraphStyle];
            _amountAttributes = [attributes copy];
            cachedAttributes = _amountAttributes;
        }
        OSSpinLockUnlock(&lock);
    }
    return _amountAttributes;
}
           

在上面的例子中,或許用不着這麼麻煩,但它示範了一種理念。我們使用了ARC的__weak來確定一旦MyTableViewCell所有的執行個體都不存在,amountAttributes會調用dealloc。是以在所有的執行個體中,我們可以持有字典的一個單獨執行個體。

這段代碼運作良好的原因是我們不太可能通路到函數内部的部分。這是很深奧的——不要在你的App中使用它,除非是你真正需要。

原文:http://webfrogs.me/2013/07/18/low-level_concurrency_apis/

繼續閱讀