IOS 多線程04-GCD詳解 底層併發 API

注:本人是翻譯過來,而且加上本人的一點看法。html

 

前言算法

想要揭示出表面之下深層次的一些可利用的方面。這些底層的 API 提供了大量的靈活性,隨之而來的是大量的複雜度和更多的責任。在咱們的文章常見的後臺實踐中提到的高層的 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. 從前
2. 延後執行
3. 隊列
4. 目標隊列
5. 資源保護
6. 單一資源的多讀單寫
7. 鎖競爭
8. 全都使用異步分發
9. 如何寫出好的異步 API
10. 迭代執行
11. 組
12. 對現有API使用 dispatchgroupt
13. 事件源
14. 監視進程
15. 監視文件
16. 定時器
17. 取消
18. 輸入輸出
19. GCD 和緩衝區
20. 讀和寫
21. 基準測試
22. 原子操做
23. 計數器
24. 比較和交換
25. 原子隊列
26. 自旋鎖網絡

 

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

  上面的 block 只會運行一次。而且在連續的調用中,這種檢查是很高效的。你能使用它來初始化全局數據好比單例。要注意的是,使用 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)中顯示,這會幫助你瞭解你的 app 是由什麼決定的:

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

  隊列能夠是並行也能夠是串行的。默認狀況下,它們是串行的,也就是說,任何給定的時間內,只能有一個單獨的 block 運行。這就是隔離隊列(原文:isolation queues。譯註)的運行方式。隊列也能夠是並行的,也就是同一時間內容許多個 block 一塊兒執行。

  GCD 隊列的內部使用的是線程。GCD 管理這些線程,而且使用 GCD 的時候,你不須要本身建立線程。可是重要的外在部分 GCD 會呈現給你,也就是用戶 API,一個很大不一樣的抽象層級。當使用 GCD 來完成併發的工做時,你沒必要考慮線程方面的問題,取而代之的,只需考慮隊列和功能點(提交給隊列的 block)。雖然往下深究,依然都是線程,可是 GCD 的抽象層級爲你慣用的編碼提供了更好的方式。

  隊列和功能點同時解決了一個接二連三的扇出的問題:若是咱們直接使用線程,而且想要作一些併發的事情,咱們極可能將咱們的工做分紅 100 個小的功能點,而後基於可用的 CPU 內核數量來建立線程,假設是 8。咱們把這些功能點送到這 8 個線程中。當咱們處理這些功能點時,可能會調用一些函數做爲功能的一部分。寫那個函數的人也想要使用併發,所以當你調用這個函數的時候,這個函數也會建立 8 個線程。如今,你有了 8 × 8 = 64 個線程,儘管你只有 8 個CPU內核——也就是說任什麼時候候只有12%的線程實際在運行而另外88%的線程什麼事情都沒作。使用 GCD 你就不會遇到這種問題,當系統關閉 CPU 內核以省電時,GCD 甚至可以相應地調整線程數量。

  GCD 經過建立所謂的線程池來大體匹配 CPU 內核數量。要記住,線程的建立並非無代價的。每一個線程都須要佔用內存和內核資源。這裏也有一個問題:若是你提交了一個 block 給 GCD,可是這段代碼阻塞了這個線程,那麼這個線程在這段時間內就不能用來完成其餘工做——它被阻塞了。爲了確保功能點在隊列上一直是執行的,GCD 不得不建立一個新的線程,並把它添加到線程池。

  若是你的代碼阻塞了許多線程,這會帶來很大的問題。首先,線程消耗資源,此外,建立線程會變得代價高昂。建立過程須要一些時間。而且在這段時間中,GCD 沒法以全速來完成功能點。有很多可以致使線程阻塞的狀況,可是最多見的狀況與 I/O 有關,也就是從文件或者網絡中讀寫數據。正是由於這些緣由,你不該該在GCD隊列中以阻塞的方式來作這些操做。看一下下面的輸入輸出段落去了解一些關於如何以 GCD 運行良好的方式來作 I/O 操做的信息。

 

4. 目標隊列

  你可以爲你建立的任何一個隊列設置一個目標隊列。這會是很強大的,而且有助於調試。

  爲一個類建立它本身的隊列而不是使用全局的隊列被廣泛認爲是一種好的風格。這種方式下,你能夠設置隊列的名字,這讓調試變得輕鬆許多—— Xcode 可讓你在 Debug Navigator 中看到全部的隊列名字,若是你直接使用 lldb(lldb) thread list 命令將會在控制檯打印出全部隊列的名字。一旦你使用大量的異步內容,這會是很是有用的幫助。

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

  默認狀況下,一個新建立的隊列轉發到默認優先級的全局隊列中。咱們就將會討論一些有關優先級的東西。

  你能夠改變你隊列轉發到的隊列——你能夠設置本身隊列的目標隊列。以這種方式,你能夠將不一樣隊列連接在一塊兒。你的 Foo 類有一個隊列,該隊列轉發到 Bar 類的隊列,Bar 類的隊列又轉發到全局隊列。

  當你爲了隔離目的而使用一個隊列時,這會很是有用。Foo 有一個隔離隊列,而且轉發到 Bar 的隔離隊列,與 Bar 的隔離隊列所保護的有關的資源,會自動成爲線程安全的。

  若是你但願多個 block 同時運行,那要確保你本身的隊列是併發的。同時須要注意,若是一個隊列的目標隊列是串行的(也就是非併發),那麼實際上這個隊列也會轉換爲一個串行隊列。

 

5. 資源保護

  多線程編程中,最多見的情形是你有一個資源,每次只有一個線程被容許訪問這個資源。它一般就是一塊內存或者一個對象,每次只有一個線程能夠訪問它。

  舉例來講,咱們須要以多線程(或者多個隊列)方式訪問 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)。

 

6. 單一資源的多讀單寫

  咱們可以改善上面的那個例子。GCD 有可讓多線程運行的併發隊列。咱們可以安全地使用多線程來從 NSMutableDictionary 中讀取只要咱們不一樣時修改它。當咱們須要改變這個字典時,咱們使用 barrier 來分發這個 block。這樣的一個 block 的運行時機是,在它以前全部計劃好的 block 完成以後,而且在全部它後面的 block 運行以前。

  以以下方式建立隊列:

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 的。

 

7. 鎖競爭

  首先,這裏有一個警告:上面這個例子中咱們保護的資源是一個 NSMutableDictionary,出於這樣的目的,這段代碼運行地至關不錯。可是在真實的代碼中,把隔離放到正確的複雜度層級下是很重要的。

  若是你對 NSMutableDictionary 的訪問操做變得很是頻繁,你會碰到一個已知的叫作鎖競爭的問題。鎖競爭並非只是在 GCD 和隊列下才變得特殊,任何使用了鎖機制的程序都會碰到一樣的問題——只不過不一樣的鎖機制會以不一樣的方式碰到。

  全部對 dispatch_asyncdispatch_sync 等等的調用都須要完成某種形式的鎖——以確保僅有一個線程或者特定的線程運行指定的代碼。GCD 某些程序上可使用時序(譯註:原詞爲 scheduling)來避免使用鎖,但在最後,問題只是稍有變化。根本問題仍然存在:若是你有大量的線程在相同時間去訪問同一個鎖或者隊列,你就會看到性能的變化。性能會嚴重降低。

  你應該從直接複雜層次中隔離開。當你發現了性能降低,這明顯代表代碼中存在設計問題。這裏有兩個開銷須要你來平衡。第一個是獨佔臨界區資源過久的開銷,以致於別的線程都由於進入臨界區的操做而阻塞。第二個是太頻繁出入臨界區的開銷。在 GCD 的世界裏,第一種開銷的狀況就是一個 block 在隔離隊列中運行,它可能潛在的阻塞了其餘將要在這個隔離隊列中運行的代碼。第二種開銷對應的就是調用 dispatch_asyncdispatch_sync 。不管再怎麼優化,這兩個操做都不是無代價的。

  使人憂傷的,不存在通用的標準來指導如何正確的平衡,你須要本身評測和調整。啓動 Instruments 觀察你的 app 忙於什麼操做。

  若是你看上面例子中的代碼,咱們的臨界區代碼僅僅作了很簡單的事情。這多是也可能不是好的方式,依賴於它怎麼被使用。

  在你本身的代碼中,要考慮本身是否在更高的層次保護了隔離隊列。舉個例子,類 Foo 有一個隔離隊列而且它自己保護着對NSMutableDictionary 的訪問,代替的,能夠有一個用到了 Foo 類的 Bar 類有一個隔離隊列保護全部對類 Foo 的使用。換句話說,你能夠把類 Foo 變爲非線程安全的(沒有隔離隊列),並在 Bar 中,使用一個隔離隊列來確保任什麼時候刻只能有一個線程使用 Foo

 

8. 全都使用異步分發

  咱們在這稍稍轉變如下話題。正如你在上面看到的,你能夠同步和異步地分發一個 block,一個工做單元。在 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();
    });
});

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

  異步調用的反作用就是它們很難調試。當咱們在調試器裏停止代碼運行,回溯並查看已經變得沒有意義了。

  要牢記這些。死鎖一般是最難處理的問題。

 

9. 如何寫出好的異步 API

  若是你正在給設計一個給別人(或者是給本身)使用的 API,你須要記住幾種好的實踐。

  正如咱們剛剛提到的,你須要傾向於異步 API。當你建立一個 API,它會在你的控制以外以各類方式調用,若是你的代碼能產生死鎖,那麼死鎖就會發生。

  若是你須要寫的函數或者方法,那麼讓它們調用 dispatch_async() 。不要讓你的函數調用者來這麼作,這個調用應該在你的方法或者函數中來作。

  若是你的方法或函數有一個返回值,異步地將其傳遞給一個回調處理程序。這個 API 應該是這樣的,你的方法或函數同時持有一個結果 block 和一個將結果傳遞過去的隊列。你函數的調用者不須要本身來作分發。這麼作的緣由很簡單:幾乎全部時間,函數調用都應該在一個適當的隊列中,並且以這種方式編寫的代碼是很容易閱讀的。總之,你的函數將會(必須)調用 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 的回調隊列。

 

10. 迭代執行

  若是你正在倒弄一些數字,而且手頭上的問題能夠拆分出一樣性質的部分,那麼 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 可能會對性能提高有所幫助,可是性能優化自己就是個很複雜的主題。維基百科上有一篇關於 Memory-bound function 的文章。內存訪問速度在 L2,L3 和主存上變化很顯著。當你的數據訪問模式與緩存大小不匹配時,10倍性能降低的狀況並很多見。

 

11. 組

  不少時候,你發現須要將異步的 block 組合起來去完成一個給定的任務。這些任務中甚至有些是並行的。如今,若是你想要在這些任務都執行完成後運行一些代碼,"groups" 能夠完成這項任務。看這裏的例子:

  首先定義group和queue

@property (nonatomic, strong) dispatch_queue_t queue_t_a;
@property (nonatomic, strong) dispatch_group_t group_t;

self.queue_t_a = dispatch_queue_create("qa", 0);
self.group_t = dispatch_group_create();

  而後運行

    dispatch_group_async(self.group_t, _queue_t_a, ^{
        sleep(3);
        NSLog(@"1");
    });
    
    dispatch_group_async(self.group_t, _queue_t_a, ^{
        NSLog(@"2");
    });
    
    dispatch_group_notify(self.group_t, _queue_t_a, ^{
        NSLog(@"3");
    });
    
    NSLog(@"viewDidAppear");

  執行打印順序永遠都是viewDidAppear、一、二、3。注意這裏只用到一個queue與group。dispatch_group_notify是等待上面全部queue a執行完以後,再執行。能夠看看官網說明

 

12. 對現有API使用 dispatchgroupt

  一旦你將 groups 做爲你的工具箱中的一部分,你可能會懷疑爲何大多數的異步API不把 dispatch_group_t 做爲一個可選參數。這沒有什麼沒法接受的理由,僅僅是由於本身添加這個功能太簡單了,可是你仍是要當心以確保本身使用 groups 的代碼是成對出現的。

  舉例來講,咱們能夠給 Core Data 的 -performBlock: API 函數添加上 groups,就像這樣:

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

  當 Core Data 上的一系列操做(極可能和其餘的代碼組合起來)完成之後,咱們可使用 dispatch_group_notify 來運行一個 block 。

  很明顯,咱們能夠給 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() 一直是成對出現的(就算有錯誤產生時)。

 

13. 事件源

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

  跟 GCD 同樣,它也是很底層的東西。當你須要用到它時,它會變得極其有用。它的一些使用是祕傳招數,咱們將會接觸到一部分的使用。可是大部分事件源在 iOS 平臺不是頗有用,由於在 iOS 平臺有諸多限制,你沒法啓動進程(所以就沒有必要監視進程),也不能在你的 app bundle 以外寫數據(所以也就沒有必要去監視文件)等等。

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

 

14. 監視進程

  若是一些進程正在運行而你想知道他們何時存在,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.

注意:在全部的事件源被傳遞到你的事件處理器以前,必須調用 dispatch_resume()

 

15. 監視文件

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

  你也可使用它來監視文件夾,好比建立一個 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 去檢測文件或者文件夾是否已經被刪除——而後就中止監聽。

 

16. 定時器

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

  須要特別重點指出的是,爲了讓 OS 節省電量,須要爲 GCD 的定時器接口指定一個低的餘地值(譯註:原文leeway value)。若是你沒必要要的指定了一個低餘地值,將會浪費更多的電量。

  這裏咱們設定了一個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);

 

17. 取消

  全部的事件源都容許你添加一個 cancel handler 。這對清理你爲事件源建立的任何資源都是頗有幫助的,好比關閉文件描述符。GCD 保證在 cancel handle 調用前,全部的事件處理都已經完成調用。

  參考上面的監視文件例子中對 dispatch_source_set_cancel_handler() 的使用。

 

18. 輸入輸出

  寫出可以在繁重的 I/O 處理狀況下運行良好的代碼是一件很是棘手的事情。GCD 有一些可以幫上忙的地方。不會涉及太多的細節,咱們只簡單的分析下問題是什麼,GCD 是怎麼處理的。

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

  在處理 I/O 時,還有一個問題就是數據是以數據塊的形式到達的。當從網絡中讀取數據時,依據 MTU([]最大傳輸單元](https://en.wikipedia.org/wiki/Maximumtransmissionunit)),數據塊典型的大小是在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) 中看到這些拷貝操做大量出現。即便你僅僅每一個內存區域拷貝一次,你仍是使用了兩倍的存儲帶寬而且佔用了兩倍的內存緩存。

 

19. GCD 和緩衝區

  最直接了當的方法是使用數據緩衝區。GCD 有一個 dispatch_data_t 類型,在某種程度上和 Objective-C 的 NSData 類型很類似。可是它能作別的事情,並且更通用。

  注意,dispatch_data_t 能夠被 retained 和 releaseed ,而且 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 拷貝到一個單獨的,更大的內存區域裏去。相反,它只是簡單地 retain 了 a 和 b。你可使用dispatch_data_apply 來遍歷對象 c 持有的內存區域:

dispatch_data_apply(c, ^bool(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 來建立一個不作任何拷貝操做的子區域。

 

20. 讀和寫

  在 GCD 的核內心,調度 I/O(譯註:原文爲 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_readdispatch_io_writedispatch_io_close。不管什麼時候數據讀完或者寫完,讀寫操做調用一個回調 block 來結束。這些都是以非阻塞,異步 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_readdispatch_write 。你須要傳遞給dispatch_read 一個文件路徑和一個在全部數據塊讀取後調用的 block。相似的,dispatch_write 須要一個文件路徑和一個被寫入的 dispatch_data_t 對象。

 

21. 基準測試

  在 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

 

22. 原子操做

  頭文件 libkern/OSAtomic.h 裏有許多強大的函數,專門用來底層多線程編程。儘管它是內核頭文件的一部分,它也可以在內核以外來幫助編程。

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

  這些函數在 atomic(3) 的幫助頁裏所有有概述——運行 man 3 atomic 命令以獲得完整的文檔。你會發現裏面討論到了內存屏障。查看維基百科中關於內存屏障的文章。若是你還存在疑問,那麼你極可能須要它。

 

23. 計數器

  OSAtomicIncrementOSAtomicDecrement 有一個很長的函數列表容許你以原子操做的方式去增長和減小一個整數值 —— 沒必要使用鎖(或者隊列)同時也是線程安全的。若是你須要讓一個全局的計數器值增長,而這個計數器爲了統計目的而由多個線程操做,使用原子操做是頗有幫助的。若是你要作的僅僅是增長一個全局計數器,那麼無屏障版本的 OSAtomicIncrement 是很合適的,而且當沒有鎖競爭時,調用它們的代價很小。

  相似的,OSAtomicOrOSAtomicAndOSAtomicXor 的函數能用來進行邏輯運算,而 OSAtomicTest 能夠用來設置和清除位。

 

24. 比較和交換

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 中若是 buffer 爲NULL。在極少的狀況下,其餘人在當前線程同時設置了 buffer ,咱們簡單地將其釋放掉。由於比較和交換方法是原子的,因此它是一個線程安全的方式去惰性初始化值。NULL的檢測和設置 buffer 都是以原子方式完成的。

  明顯的,使用 dispatch_once() 咱們也能夠完成相似的事情。

 

25. 原子隊列

  OSAtomicEnqueue()OSAtomicDequeue() 可讓你以線程安全,無鎖的方式實現一個LIFO隊列(常見的就是棧)。對有潛在精確要求的代碼來講,這會是強大的代碼。

  還有 OSAtomicFifoEnqueue()OSAtomicFifoDequeue() 函數是爲了操做FIFO隊列,但這些只有在頭文件中才有文檔 —— 閱讀他們的時候要當心。

 

26. 自旋鎖

  最後,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 中使用它。

 

能夠關注本人的公衆號,多年經驗的原創文章共享給你們。

相關文章
相關標籤/搜索