GCD

一. GCD和多線程的介紹

GCD

GCD是異步執行任務的技術之一。開發者只需要定義想執行的任務并追加到適當的Dispatch Queue中,GCD就能生成必要的線程并計劃執行任務。由于線程管理是作為系統的一部分來實現的,因此可統一管理,也可執行任務,比起使用NSThread類,performSelector系方法更為簡潔,執行效率更高。

dispatch_async(queue, ^{
        /*
         * 長時間處理
         * 例如AR用畫像識別,數據庫訪問
         */
        
        // 長時間處理結束,主線程使用該處理結果
        dispatch_async(dispatch_get_main_queue(), ^{
            /*
             * 只有在主線程可以執行的結果
             * 例如用戶界面更新
             */
        });
    });
多線程編程

iOS,OS X操作系統啟動應用程序后,首先便將包含在應用程序中的CPU命令列配置到內存中。CPU從應用程序指定的地址開始,一個一個地執行CPU命令列。但是,由于一個CPU一次只能執行一個命令,不能執行某處分開的并列的兩個命令。


通過CPU執行的CPU命令行.png

“1個CPU執行的CPU命令列為一條無分叉路徑”即為“線程”。如果存在多條即為多線程。在多線程中,1個CPU核執行多條不同路徑上的不同命令。


在多線程中執行CPU命令列.png

現在基本上1個CPU核一次能夠執行的CPU命令始終為1。對于單核來說,由于使用多線程的程序在某個線程和其他線程之間反復多次進行上下文切換,因此看上去好像1個CPU核能并列執行多個線程。對于多個CPU核來說,就是真的提供多個CPU核并行執行多個線程的技術。
但是,多線程編程如果處理不好也容易發生各種問題:
  • 多個線程更新相同的資源會導致數據的不一致(數據競爭)
  • 停止等待事件的線程會導致多個線程相互持續等待(死鎖)
  • 使用太多線程會消耗大量內存
    比如,應用程序在啟動時,通過最先執行的線程,即“主線程”來描繪用戶界面、處理觸摸屏幕的事件等。如果在主線程進行長時間的處理,就會妨礙主線程的執行(阻塞)。在iOS的應用程序中,會妨礙主線程中的RunLoop的主循環的執行,從而導致不能更新用戶界面、應用程序的畫面長時間停滯等問題。


    多線程編程的優點.png

    使用多線程編程,在執行長時間的處理時仍可保證用戶界面的響應。

二. GCD的API

Dispatch Queue

開發者要做的只是定義想執行的任務并追加到適當的Dispatch Queue中。Dispatch Queue是執行處理的等待隊列,其按照追加的順序執行處理。


通過Dispatch Queue執行處理.png

在執行處理時,存在兩種Dispatch Queue。一種是等待現在執行中處理的Serial Dispatch Queue,另一種是不等待現在執行中處理的Concurrent Dispatch Queue。


Dispatch Queue的種類.png
dispatch_queue_create

通過GCD的API生成的Dispatch Queue。

dispatch_queue_t mySerialDipathQueue = dispatch_queue_create("com.example.www", NULL);//默認Serial Dipath Queue

Concurrent Dispatch Queue并行執行多個追加處理,而Serial Dispatch Queue同時只能執行1個追加處理。雖然Serial Dispatch Queue和Concurrent Dispatch Queue收到系統資源的限制,但用dispatch_queue_create函數可生成任意多個Dispatch Queue。
當生成多個Serial Dispatch Queue時,各個Serial Dispatch Queue并行執行。雖然在1個Serial Dispatch Queue中同時只能執行一個追加處理,但如果將處理分別追加到4個Serial Dispatch Queue中,各個Serial Dispatch Queue執行1個,即為同時執行4個處理。


多個Serial DispatchQueue.png

如果生成Serial Dispatch Queue并追加處理,系統對于一個Serial
Dispatch Queue就只生成并使用一個線程,如果生成2000個Serial
Dispatch Queue,就生成2000個線程。如果過多使用多線程,就會消耗大量內存,引起大量上下文切換,大幅度降低系統的響應性能。

當在避免多個線程更新相同資源導致數據競爭時使用Serial
Dispatch Queue。且Serial
Dispatch Queue的生成個數應該僅限所需的個數。比如更新數據庫時一個表生成一個Serial
Dispatch Queue。


Serial Dispatch Queue的用途.png

當想并行執行不發生數據競爭等問題的處理時,使用Concurrent Dispatch Queue。對于Concurrent Dispatch Queue,線程數由XNU內核決定和管理。

//第一個參數推薦逆序全程域名,第二個參數指定串行或并行
dispatch_queue_t myConcurrentDipathQueue = dispatch_queue_create("com.example.www", DISPATCH_QUEUE_CONCURRENT);
//在隊列中追加任務 
dispatch_async(myConcurrentDipathQueue, ^{
     NSLog(@"myConcurrentDipathQueue");
});
//需要程序員負責釋放
dispatch_release(myConcurrentDipathQueue);

Dispatch Queue像Objective-C的引用計數管理一樣,需要通過dispatch_retain函數和dispatch_release函數的引用計數來管理內存。在dispatch_async函數中追加Block到Dispatch Queue后,即使立即釋放Dispatch Queue,該Dispatch Queue由于被Block所持有也不會被廢棄,因此Block能夠執行。Block執行結束后釋放Dispatch Queue,這時誰都不持有Dispatch Queue,因此它被廢棄。

Main Dispatch Queue/Global Dispatch Queue

Main Dispatch Queue和Global Dispatch Queue是系統提供的,不用我們主動去生成。

Main Dispatch Queue
Main Dispatch Queue是在主線程中執行的Dispatch Queue。因為主線程只有1個,所以Main Dispatch Queue自然就是Serial Dispatch Queue。追加到Main Dispatch Queue的處理在主線程的RunLoop中執行。由于在主線程執行,因此要將用戶界面的界面更新等一些必須在主線程中執行的處理追加到Main Dispatch Queue使用。

Global Dispatch Queue
Global Dispatch Queue是所有應用程序都能夠使用的Concurrent Dispatch Queue。沒有必要通過dispatch_queue_create 函數逐個生成Concurrent Dispatch Queue。只要獲取Global Dispatch Queue使用即可。
Global Dispatch Queue有4個執行優先級。通過XNU內核管理用于Global Dispatch Queue的線程,將各自使用的Global Dispatch Queue的執行優先級作為線程的執行優先級使用。

Dispatch Queue的種類.png

對于Main Dispatch Queue和Global Dispatch Queue,不用我們主動地進行dispatch_retain和dispatch_release(內部已實現)。

//在默認優先級的Global Dispatch Queue中執行Block
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     /*
      * 可并行執行的處理
      */
        
     //在Main Dispatch Queue中執行Block
     dispatch_async(dispatch_get_main_queue(), ^{
          /*
           * 只能載主線程執行的處理
           */
      });
});

總結:
我們在代碼中沒看到關于線程的字眼,實際是由隊列和異步同步去影響的。
dispatch_sync:同步任務無論在自定義串行隊列、自定義并行隊列、主隊列(當前線程為主線程時會出現死鎖)、全局隊列 執行任務時,都不會創建子線程,而是在當前線程中串行執行;
dispatch_async:異步任務無論在自定義串行隊列、自定義并行隊列(主隊列除外,主隊列下,任務會在主線中串行執行)、全局隊列 執行任務時,都會創建子線程,并且在子線程中執行;
比如,dispatch_sync同步派發情況下,一條串行隊列對應一條線程(比如主隊列就是串行隊列,就在主線程)。dispatch_async同步派發情況下,一條并行隊列可能對應很多線程,這個由CPU決定。(比如全局隊列就是并行隊列,放入全局隊列,任務由CPU調度并行執行)。

dispatch_set_target_queue

dispatch_queue_create函數生成的Dispatch Queue 不管是Serial Dispatch Queue還是Concurrent Dispatch Queue,都使用與默認優先級Global Dispatch Queue相同執行優先級的線程。變更生成的Dispatch Queue的執行優先級要使用dispatch_set_target_queue函數。

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.www", NULL);
dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

//將mySerialDispatchQueue的執行優先級改成和globalDispatchQueue執行優先級一樣
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueue);

將Dispatch Queue 指定為dispatch_set_target_queue 函數的參數,不僅可以變更Dispatch Queue的執行優先級,還可以作成Dispatch Queue的執行階層。比如,在必須將不可并行執行的處理追加到多個Serial Dispatch Queue中時,如果使用dispatch_set_target_queue 函數將目標指定為某一個Serial Dispatch Queue,即可防止并行執行。

dispatch_after

想在指定時間后處理任務,可使用dispatch_after函數來實現。

//常用DISPATCH_TIME_NOW,從現在開始
//ull unsigned long long, NSEC_PER_SEC  秒(NSEC_PER_MSEC  毫秒)
//也可以用dispatch_walltime ,主要用于指定時間點
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

dispatch_after(time, dispatch_get_main_queue(), ^{
     NSLog(@"waited at least three seconds");
});

dispatch_after函數不是在指定的時間后執行處理,而是在指定時間追加處理到Dispatch Queue。因為Main Dispatch Queue 在主線程的RunLoop中執行,所以在每隔1/60秒執行的RunLoop中,Block最快在3秒后執行,最慢在3秒+1/60后執行,并且在Main Dispatch Queue 中有大量處理追加或主線程的處理本身有延遲時,這個時間會更長。

Dispatch Group

在追加到Dispatch Queue中的多個處理全部結束后想執行結束處理時,如果只使用一個Serial Dispatch Queue,只要將想執行的處理全部追加到該Serial Dispatch Queue中并在最后追加結束處理。如果是使用Concurrent Dispatch Queue或同時使用多個Dispatch Queue,就得使用
Dispatch Group。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
    
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
dispatch_release(group);

也可以使用dispatch_group_wait函數僅等待全部處理執行完畢。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
    
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});

long result = dispatch_group_wait(group, time);

if (result == 0) {
        //屬于Dispatch Queue的全部處理執行完畢
    }else{
        //屬于Dispatch Queue的某一個處理還在執行
    }
dispatch_release(group);

當等待時間time為DISPATCH_TIME_FOREVER,返回結果恒為0。這里的等待意味著一旦調用dispatch_group_wait,該函數就處于調用的狀態而不返回(到達等待時間才會返回),即執行dispatch_group_wait函數的現在的線程停止。

dispatch_barrier_async

dispatch_barrier_async函數會等待追加到Concurrent Dispatch Queue上的并行執行的處理全部結束后,再將指定的處理追加到該Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函數追加的處理執行完畢后,Concurrent Dispatch Queue才恢復為一般的動作,追加到該Concurrent Dispatch Queue的處理又開始并行執行。

Dispatch_barrier_async函數的處理流程.png

比如在訪問數據庫時候,在多個讀取操作中插入寫入操作。
在blk4_for_reading和blk5_for_reading處理之間執行寫入處理,并將寫入的內容讀取blk5_for_reading處理以及之后的處理中。

dispatch_queue_t queue = dispatch_queue_create("com.example.www", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
    
//加入寫入處理,后面讀取的內容是該結果
dispatch_barrier_async(queue, blk_for_writing);
    
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);
dispatch_async(queue, blk8_for_reading);

使用Concurrent Dispatch Queue 和 dispatch_barrier_async函數可實現高效率的數據庫訪問和文件訪問。

dispatch_sync

dispatch_async的 async意味著非同步,就是將指定的Block非同步地追加到指定的Dispatch Queue中。dispatch_async函數不做任何等待。

Dispatch_async函數的處理流程.png

dispatch_async的 sync意味著同步,就是將指定的Block同步地追加到指定的Dispatch Queue中。dispatch_async函數會一直等待。

Dispatch_sync函數的處理流程.png

一旦調用dispatch_async函數,在指定的處理執行結束之前,該函數不會返回(類似dispatch_group_wait)。dispatch_asyn常用于非主線程。例如在主線程執行以下源碼會造成死鎖。

dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"hello")};);

該源碼在主線程中執行指定的Block,并等待其執行結束。而其實在主線程中正在執行這些源代碼,所以無法執行追加到Main Dispatch Queue的Block。下面的例子也是同理:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
     dispatch_sync(queue, ^{NSLog(@"hello");});
});

在Serial Dispatch Queue也會引起相同的問題:

dispatch_queue_t queue = dispatch_queue_create("com.example.www", NULL);
    dispatch_async(queue, ^{
        dispatch_sync(queue, ^{NSLog(@"hello");});
    });

我的理解:主線程此時正在處理當前隊列,并且阻塞在dispatch_sync,而dispatch_sync函數又將一個新的任務提交到主隊列排隊執行,然后主線程這個時候要處理完當前任務才能取出新的任務進行執行,這樣導致死鎖。
所以在使用dispatch_sync函數等同步等待處理執行的API時,要深思熟慮。
在主隊列同步執行任務的情況就好像下面:

dispatch_queue_t serialQueue = dispatch_queue_create("com.blbl", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(serialQueue, ^{
        NSLog(@"任務A");
        /**
         在當前隊列又提交一次同步運行的block,
         導致任務A需要等待任務B返回,而任務A在任務B之前調用,
         所以任務B又需要等待任務A返回了之后才能執行
         */
        dispatch_sync(serialQueue, ^{
            NSLog(@"任務B");
        });
    });
dispatch_apply

dispatch_apply函數是dispatch_sync函數和Dispatch Group的關聯API。該函數按指定的次數將指定的Block追加到指定的Dispatch Queue中,并等待全部處理執行結束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(10, queue, ^(size_t index){
    NSLog(@"%zu",index);
});
NSLog(@"done");
//輸出結果:  4 1 6 8 2 9 0 5 3 7 done

因為在Global Dispatch Queue中執行處理,所以各個處理執行時間不定,但是最后一定是輸出done,因為dispatch_apply函數會等待全部結果執行結束。
另外,由于dispatch_apply與dispatch_sync函數相同,會等待處理執行結束,也推薦在dispatch_async中非同步地執行dispatch_apply函數。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//在global queue中非同步執行
dispatch_async(queue, ^{
    dispatch_apply([array count], queue, ^(size_t index){
        /*
         * 并列處理包含在NSArray對象的全部對象
         */
    });
    
    //等待dispatch_apply函數中的處理全部執行結束,跳轉到main queue中非同步執行
    dispatch_async(dispatch_get_main_queue(), ^{
        /*
         * 用戶界面更新等
         */
    });
});
dispatch_suspend/dispatch_resume

當追加大量處理到Dispatch Queue時,在追加處理的過程中,有時希望不執行已追加的處理。這種情況下,只要掛起Dispatch Queue即可,當可以執行時再恢復。

//掛起指定的queue
dispatch_suspend(queue);

//恢復指定的queue
dispatch_resume(queue);

這些函數對已經執行的處理沒有影響。掛起后,追加到Dispatch Queue中但尚未執行的處理在此之后停止執行。恢復后使得這些處理能夠繼續執行。

Dispatch Semaphore

當并行執行的處理更新數據時,會產生數據不一致的情況,雖然使用Serial Dispatch Queue和dispatch_barrier_async函數可以避免這類問題。但是要想進行更細粒度的排他控制就要使用Dispatch Semaphore。
Dispatch Semaphore是持有計數的信號,該計數是多線程編程中的計數類型信號。在Dispatch Semaphore中,使用計數來實現該功能。計數0時等待,計數1或大于1時,減去1而不等待。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

/*
 * 生成計數初始值1的Dispatch Semaphore
 * 保證可訪問array對象的線程只有1個
 */
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

NSMutableArray *array = [[NSMutableArray alloc] init];

for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        /*
         *一直等待,直到Dispatch Semaphore的計數值達到大于等于1
         */
        
        //當Dispatch Semaphore的計數值大于等于1執行到這一步,
        //將計數值減1,并且執行返回。(返回值同dispatch_group_wait一樣)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        
        //所以只有1個線程能做該操作
        [array addObject:[NSNumber numberWithInt:i]];
        
        //將Dispatch Semaphore的計數值加1
        dispatch_semaphore_signal(semaphore);
    });
}

//只要有crate就要我們去release,類似Dispatch Group
dispatch_release(semaphore);
dispatch_once

通過dispatch_once函數,即使在多線程環境下執行也保證線程安全。所以常用于生成單例對象。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    /*
     * 初始化
     */
});
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容