iOS多線程-GCD的基礎概念和API

GCD是蘋果推出的一套從底層線程編程中抽象出的一種基于隊列來管理任務的方式。相對于直接在線程上執(zhí)行任務,使用GCD的方式更加簡便和高效。

通過GCD,調用者可以同步或者異步將要執(zhí)行的任務放進相應的隊列中等待執(zhí)行,隊列采用先進先出的方式取出里面的任務,然后將任務分配到相應的線程中執(zhí)行。

理解GCD需要理解幾個關鍵的概念和問題:

dispatch_async和dispatch_sync(異步執(zhí)行和同步執(zhí)行)

同步執(zhí)行和異步執(zhí)行是相對于當前調用的線程而言的。也就是說,dispatch_sync會阻塞當前的線程而dispatch_async不會。

并發(fā)隊列和串行隊列

串行隊列中的任務是一個個按順序完成的。后一個任務必須等待前一個任務完成。而并發(fā)隊列中的任務是可以同時進行的。同時進行意味者在并發(fā)隊列中同時進行的任務一定是在不同的線程中執(zhí)行的。

隊列和線程的關系

GCD提供了一個串行的主隊列和若干個全局并發(fā)隊列。主隊列中的所有任務都會在主線程中執(zhí)行,而全局并發(fā)隊列會根據應用當前執(zhí)行的情況來給任務分配線程,包括主線程。

重要:雖然下面會大量地提到線程,但是GCD的核心在于如何通過隊列的方式來思考和執(zhí)行相應的任務來達到想要的效果,大部分時候都可以忽略線程,但是在學習的時候我們可以多考慮一下,加深對GCD的理解

自己創(chuàng)建的隊列中的任務最終會進入到默認優(yōu)先級的全局隊列或者主隊列。關系可見官方的配圖:
gcd_queue_thread

可以通過一些簡單的代碼來驗證一下上面的內容:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.async", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"async block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);

運行結果如下:

2017-04-18 17:34:56.905 ConcurrentProgram[89385:39591640] Before excute: <NSThread: 0x6180000678c0>{number = 1, name = main}
2017-04-18 17:34:56.906 ConcurrentProgram[89385:39591640] After excute: <NSThread: 0x6180000678c0>{number = 1, name = main}
2017-04-18 17:34:58.911 ConcurrentProgram[89385:39593236] async block 2: <NSThread: 0x61800006c9c0>{number = 3, name = (null)}
2017-04-18 17:34:59.910 ConcurrentProgram[89385:39593020] async block 1: <NSThread: 0x60000006ffc0>{number = 4, name = (null)}

分析:Before excute和After excute都是在主線程中完成的。在兩者間主線程調用了兩次dispatch_async方法來將兩個任務,block1和block2放入自定義的并發(fā)隊列中。從運行結果看,先完后After Excute,說明主線程的執(zhí)行并沒有因為調用dispatch_async而阻塞。從時間上看,block2先于block1完成,并且二者相差一秒,剛好是兩者sleep的時間的差。說明這兩個任務是同時執(zhí)行的,這一點還可以在打印出來的線程地址不一致中得到印證。

過程如下:

  1. 主線程打印Before excute
  2. 主線程將block1放入自定義并發(fā)隊列中異步執(zhí)行,并發(fā)隊列開線程A執(zhí)行block1,
  3. 主線程繼續(xù)執(zhí)行,將block2放入并發(fā)隊列中異步執(zhí)行,并發(fā)隊列取出block2,再開辟新的線程B執(zhí)行block2
  4. 主線程繼續(xù)執(zhí)行,打印After excute
  5. B線程在休眠兩秒后打印
  6. A線程在休眠3秒后打印

再看下一個示例:

- (void)dispatchSyncFromMainToConcurrentQueue{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent.sync", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"sync block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);
}

結果如下:

2017-04-19 10:31:51.647 ConcurrentProgram[29439:40557792] Before excute: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:54.648 ConcurrentProgram[29439:40557792] async block 1: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] sync block 2: <NSThread: 0x61000006d840>{number = 1, name = main}
2017-04-19 10:31:56.649 ConcurrentProgram[29439:40557792] After excute: <NSThread: 0x61000006d840>{number = 1, name = main}

過程如下:

  1. 主線程打印before excute
  2. 主線程將block1放入并發(fā)隊列中同步執(zhí)行,此時主線程阻塞
  3. 并發(fā)隊列取出block1,將block1分配給主線程執(zhí)行,block1打印
  4. block1執(zhí)行完畢后,主線程繼續(xù)執(zhí)行,將block2放入隊列中,
  5. 隊列將block2分配給主線程執(zhí)行,block2打印
  6. block2執(zhí)行完畢后主線程繼續(xù)執(zhí)行,打印after excute

結論:

  1. 放進同一個并發(fā)隊列中的不同任務不一定就會分配到不同的線程
  2. 并發(fā)隊列也可能將隊列中的任務交由主線程執(zhí)行

推測:在調用dispatch_sync時,由于調用的線程阻塞,或者在等待dispatch的任務完成之前是空閑的,那么dispatch的隊列很可能將該任務交由調用的線程執(zhí)行,這也是有些文章在說dispatch_sync和dispatch_async的區(qū)別是會說一個不會新開線程一個會新開線程,其實這個說法不正確。如果dispatch_sync或者dispatch_async的隊列參數是主隊列,那么一定會放進主線程中執(zhí)行,此時不存在開不開線程的問題。dispatch_sync和dispatch_async的根本區(qū)別還是在于是否會阻塞調用該方法的線程。

再看一下串行隊列的例子

- (void)serialQueue{
    dispatch_queue_t serialQueue = dispatch_queue_create("serial", nil);
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(serialQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"async block 1: %@",[NSThread currentThread]);
    });
    
    dispatch_sync(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"sync block 2: %@",[NSThread currentThread]);
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);
}
2017-04-19 10:58:56.296 ConcurrentProgram[30778:40656851] Before excute: <NSThread: 0x618000074740>{number = 1, name = main}
2017-04-19 10:58:59.297 ConcurrentProgram[30778:40657162] async block 1: <NSThread: 0x60000007ea40>{number = 3, name = (null)}
2017-04-19 10:59:01.298 ConcurrentProgram[30778:40656851] sync block 2: <NSThread: 0x618000074740>{number = 1, name = main}
2017-04-19 10:59:01.299 ConcurrentProgram[30778:40656851] After excute: <NSThread: 0x618000074740>{number = 1, name = main}

過程:

  1. 主線程打印before
  2. 主線程將block1放入串行隊列中異步執(zhí)行,串行隊列取出任務,開辟線程A執(zhí)行block1(由于主線程此時是不阻塞的,還要繼續(xù)執(zhí)行,因此必須新開一個線程執(zhí)行block1)
  3. 此時主線程繼續(xù)執(zhí)行,將block2放入串行隊列中同步執(zhí)行,主線程阻塞,等待block2執(zhí)行完畢。
  4. 由于是串行隊列,block2必須等待block1執(zhí)行完畢。
  5. 線程A執(zhí)行block1完畢,串行隊列取出block2并分配給主線程執(zhí)行并打?。ㄒ驗閎lock2執(zhí)行完之前主線程必須等待,讓主線程執(zhí)行完全沒有問題,省下開辟線程的開銷)
  6. 最后主線程打印aftert

一個常見的用法:

    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //do some calcutate
        [NSThread sleepForTimeInterval:3];
        NSLog(@"block calculate: %@",[NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"main block upadte: %@",[NSThread currentThread]);
        });
    });
    NSLog(@"After excute: %@",[NSThread currentThread]);

在主線程的執(zhí)行過程中異步地將一個需要耗時的任務放入全局并發(fā)隊列,此時隊列會使用非主線程執(zhí)行該任務,等待任務完成后在將更新UI的任務放入主隊列,主隊列分配給主線程執(zhí)行更新UI的任務。

一個不要犯的錯誤:

    dispatch_queue_t queue = dispatch_queue_create("wrong", nil);
    NSLog(@"Before excute: %@",[NSThread currentThread]);
    dispatch_async(queue, ^{
        NSLog(@"block 1: %@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"block2 %@",[NSThread currentThread]);
        });
        NSLog(@"after block2");
    });
    
    NSLog(@"After excute: %@",[NSThread currentThread]);

會造成EXEC_BAD_INSTRUCTION的錯誤。在官方文檔中有這樣一句提示:

Important: You should never call the dispatch_sync or dispatch_sync_f function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.

永遠不要在一個隊列中調用dispatch_sync方法到同一個隊列中,尤其是在串行隊列(一定死鎖),在并發(fā)隊列中也應該避免使用。

來分析一下為什么:

串行隊列正在執(zhí)行任務A,中間調用了dispatch_async要分派一個新的任務B(block)給隊列自己,此時正在執(zhí)行的線程被阻塞等待任務B執(zhí)行,但是任務B在串行隊列中排在任務A后面需要等待A完成后執(zhí)行。于是B等待A,A等待B。死鎖。

并發(fā)隊列雖然沒有上述死鎖問題(因為并發(fā)隊列不需要等待上一個任務完成)。要避免這樣寫估計是習慣問題。因為大多數情況下dispatch到其他的隊列效果是一樣的而不會造成誤解。

dispatch_barrier_async

dispatch_barrier_async的意義在于,對于并發(fā)隊列而言,在某些時候可以像串行隊列一樣控制任務的執(zhí)行順序。

假設有這樣的需求,任務A執(zhí)行30s后得到結果A1,任務B需要執(zhí)行20s后得到結果B1,任務C的運算依賴于A1 和 B1,得到結果C1,任務D的處依賴于C1。那么我們要保證任務C在任務A和B執(zhí)行完之后,D在任務C執(zhí)行完以后才能有正確的結果。當然我們可以將ABCD放進一個串行隊列,但是如果A和B都是非常耗時的任務,那么會造成計算時間上的浪費。因為A和B是可以并發(fā)計算的。這個時候采用dispatch_barrier_async就非常有意義了。

    dispatch_queue_t queue = dispatch_queue_create("My concurrent queue", DISPATCH_QUEUE_CONCURRENT);
    __block int a1 = 0;
    __block int b1 = 0;
    __block int c1 = 0;
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:3];
        a1 = 5;
        NSLog(@"A finish");
    });
    
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        b1 = 10;
        NSLog(@"B finish");
    });
    
    dispatch_barrier_async(queue, ^{
        c1 = a1+b1;
        NSLog(@"handle A1 and B1");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"handle C1");
    });

運行結果為:

2017-04-19 15:18:32.106 ConcurrentProgram[41116:41124691] B finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] A finish
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41123629] handle A1 and B1
2017-04-19 15:18:33.106 ConcurrentProgram[41116:41124651] handle C1

dispatch_barrier_async中的任務會等待其隊列中在它之前的任務完成后執(zhí)行,并且在該任務后面加入隊列的任務(如最后一個block)會等待該任務執(zhí)行完之后才會開始執(zhí)行。

這個API文檔中的discussion里有這樣一句話:

The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

也就是說這個API只有對于自己創(chuàng)建的并發(fā)隊列是有用的,否則就和dispatch_async效果是一樣的。

我們考慮一下如果是串行隊列,那么本來里面的任務就是必須按順序一個接一個完成,所以使用這個API是沒有意義的。問題在于為什么系統提供的全局并發(fā)隊列無法達到相應的效果呢?

個人猜測是這樣的:上面說過,自己創(chuàng)建的隊列中的任務最終會放入到系統的全局隊列中或者主隊列中,那么這個API可能只是控制了在什么時機將隊列中的任務放進全局隊列中去。采用剛剛的例子,就是自己創(chuàng)建的queue中有ABCD四個任務,queue將A和B先放入全局隊列,等待A和B都執(zhí)行完以后,將C放進全局隊列,C執(zhí)行完成后再將D放進全局隊列中。所以全局隊列中本身是沒有這種機制的。

參考資料:

https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW2

https://objccn.io/issue-2-2/

http://www.lxweimin.com/p/06a18323d9d2

http://www.humancode.us/2014/08/06/concurrent-queues.html

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

推薦閱讀更多精彩內容