深度理解GCD執行緒死鎖,佇列,同步和非同步,序列和併發

Deft_MKJing宓珂璟發表於2016-12-04

介紹GCD

可以先看看這個

“併發”指的是程式的結構,“並行”指的是程式執行時的狀態
https://blog.csdn.net/sinat_35512245/article/details/53836580
併發是能力

並行是狀態

並行指物理上同時執行,併發指能夠讓多個任務在邏輯上交織執行的程式設計(cpu時間片輪轉優先順序排程)

Grand Central Dispatch (GCD) 是 Apple 開發的一個多核程式設計的解決方法。該方法在 Mac OS X 10.6 雪豹中首次推出,並隨後被引入到了 iOS4.0 中。GCD 是一個替代諸如 NSThread, NSOperationQueue, NSInvocationOperation 等技術的很高效和強大的技術。

任務和佇列

看下最簡單的GCD非同步把任務加入全域性併發佇列的程式碼
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任務");
});
  • 任務
    任務其實就是一段想要執行的程式碼,在GCD中就是Block,就是C程式碼的閉包實現,需要詳細瞭解Block的請戳2分鐘明白Block,因此,做法非常簡單,例如reloadTableView 就可以加到這裡去,問題在於任務的執行方式同步執行非同步執行 ,這兩者最簡單可以概括為 是否具有開執行緒的能力
    展開來說就是是否會阻塞當前執行緒,如果和上面示例程式碼所示,是async,他不會阻塞當前執行緒,block裡面的任務會在另一個執行緒執行,當前執行緒會繼續往下走,如果是sync,那麼Block裡面的任務就會阻塞當前執行緒,該執行緒之後的任務都會等待block的任務執行完,如果比較耗時,執行緒就會處於假死狀態
  • 佇列
    上面講的是任務的同步執行或者非同步執行,那麼佇列就是用於任務存放,分別有序列佇列並行佇列
    佇列都遵循FIFO(first in first out),序列佇列根據先進先出的順序取出來放到當前執行緒中,二並行佇列會把任務取出來放到開闢的非當前執行緒,也就非同步執行緒中,任務無限多的時候不會開無限個執行緒,會根據系統的最大併發數進行開執行緒

簡單概括如下:

專案 同步(sync) 非同步(async)
序列 當前執行緒,順序執行 另一個執行緒,順序執行
併發 當前執行緒,順序執行 另一個執行緒,同時執行

可以看出同步和非同步就是開執行緒的能力,同步執行必然一個個順序執行在當前執行緒,而非同步執行可以根據佇列不同來確定順序還是同步併發執行

佇列的建立 和 簡單API

  • 主佇列:dispatch_get_main_queue(); 主執行緒中序列佇列
  • 全域性佇列:dispatch_get_global_queue(0, 0); 全域性並行佇列
  • 自定義佇列

     // 自定義序列佇列
    dispatch_queue_create(@"custom name of thread", DISPATCH_QUEUE_SERIAL);
    // 自定義併發佇列
    dispatch_queue_create(@"com.mkj.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
  • 最簡單的API

    dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>, ^(void)block)
    dispatch_async(<#dispatch_queue_t  _Nonnull queue#>, ^(void)block)
    

執行緒死鎖1

NSLog(@"任務1");
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"任務2");
});
NSLog(@"任務3");

列印資訊

2016-12-04 12:50:55.932 GCD[3020:116100] 任務1
(lldb) 
Exc_bad_INSTRUCTION報錯

分析:
首先執行任務1,然後遇到dispatch_sync 同步執行緒,當前執行緒進入等待,等待同步執行緒中的任務2執行完再執行任務3,這個任務2是加入到mainQueue主佇列中(上面有提到這個是同步執行緒),FIFO原則,主佇列加入任務加入到隊尾,也就是加到任務3之後,那麼問題就來了,任務3等待任務2執行完,而任務2加入到主佇列的時候,任務2就會等待任務3執行完,這個就趙成了死鎖。
這裡寫圖片描述

根據上面的描述,可以看到,當前執行緒中,如果開啟同步,而且把任務加入到當前執行緒,那麼當前執行緒就會阻塞
看下如下的案例
案例一
我們在當前執行緒中,開啟同步等待,然後把任務加入到自定義的序列佇列中,這個時候任務一執行完之後,程式等待,任務2被放入序列佇列(不是當前佇列主佇列中),那麼另外開闢的序列佇列執行任務2,然後繼續執行任務3,不會有死迴圈

dispatch_queue_t t = dispatch_queue_create("com.mikejing", DISPATCH_QUEUE_SERIAL);
    NSLog(@"任務1");
    dispatch_sync(t, ^{
        NSLog(@"任務2");
    });
    NSLog(@"任務3");
    2018-09-07 21:52:03.290198+0800 inherit[1640:22106] 任務1
    2018-09-07 21:52:03.290379+0800 inherit[1640:22106] 任務2
    2018-09-07 21:52:03.290450+0800 inherit[1640:22106] 任務3

案例二
依然是同步等待,我們這個時候把任務2放在全域性併發佇列裡面,這個時候,一樣同步等待,等待併發佇列任務2執行完,再執行任務3,這裡等待的是併發佇列,不會阻塞當前執行緒
主執行緒中 同步併發佇列

NSLog(@"任務1");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"任務2");
    });
    NSLog(@"任務3");
    2018-09-07 23:12:53.943102+0800 inherit[4350:71057] 任務1
    2018-09-07 23:12:53.943256+0800 inherit[4350:71057] <NSThread: 0x60c000079c80>{number = 1, name = main}
    2018-09-07 23:12:53.943345+0800 inherit[4350:71057] 任務2
    2018-09-07 23:12:53.943433+0800 inherit[4350:71057] 任務3

子執行緒中同步併發佇列

//    dispatch_queue_t t = dispatch_queue_create("com.mikejing", DISPATCH_QUEUE_SERIAL);
//    // 非同步序列
//    dispatch_async(t, ^{
//        NSLog(@"任務1%@",[NSThread currentThread]);
//        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//            NSLog(@"任務2%@",[NSThread currentThread]);
//        });
//        NSLog(@"任務3%@",[NSThread currentThread]);
//    });
//    2018-09-07 22:11:50.201285+0800 inherit[2421:38097] 任務1<NSThread: 0x60400027d740>{number = 3, name = (null)}
//    2018-09-07 22:11:50.201442+0800 inherit[2421:38097] 任務2<NSThread: 0x60400027d740>{number = 3, name = (null)}
//    2018-09-07 22:11:50.201566+0800 inherit[2421:38097] 任務3<NSThread: 0x60400027d740>{number = 3, name = (null)}

案例二告訴我們,只要是同步,就不會開闢執行緒,無論是序列佇列還是併發佇列,都會等待,然後會有執行緒自身排程去執行序列中的任務或者併發列隊中的任務,可以理解為,同步的前提下,序列佇列和併發佇列是一樣的,因為同步,反正需要一個一個執行。

執行緒死鎖2

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);

NSLog(@"任務1");
dispatch_async(serialQueue, ^{
    NSLog(@"任務2");
    dispatch_sync(serialQueue, ^{
        NSLog(@"任務3");
    });
    NSLog(@"任務4");
});
NSLog(@"任務5");

列印日誌:

2016-12-04 13:10:06.587 GCD[3322:129782] 任務1
2016-12-04 13:10:06.588 GCD[3322:129782] 任務5
2016-12-04 13:10:06.588 GCD[3322:129815] 任務2
(lldb)  同樣在這裡報錯停止

分析
1.這裡用系統create的方法建立自定義執行緒,按順序先執行任務1

2.然後遇到一個非同步執行緒,把任務2,同步執行緒(包含任務3),任務4這三個東西看成一體放到自定義的序列佇列中,由於是非同步執行緒,直接執行下一個任務5,因此非同步執行緒的任務2和任務5不確定誰先誰後,但是任務1 任務2 任務5這三個東西必定會列印出來

3.看下非同步執行緒裡面,都放置在自定義的序列佇列中,任務2之後遇到一個同步執行緒,那麼執行緒阻塞,執行同步執行緒裡面的任務3,由於這個佇列裡面放置的任務4按第二步裡面的順序率先加入進序列佇列的,當同步執行緒執行的時候,裡面的任務3是還是按照FIFO順序加入到任務4之後,那麼又造成了案例一里面的任務4等待任務3,任務3等待任務4的局面,又死鎖了
這裡寫圖片描述這裡寫圖片描述

這裡寫圖片描述

執行緒之間的排程,安全避開死鎖

NSLog(@"任務1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任務2");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任務3");
    });
    NSLog(@"任務4");

});
NSLog(@"任務5");

列印日誌:這裡不會產生死鎖,直接解釋下如何排程

2016-12-04 13:34:56.136 GCD[3726:150720] 任務1
2016-12-04 13:34:56.137 GCD[3726:150720] 任務5
2016-12-04 13:34:56.137 GCD[3726:150765] 任務2
2016-12-04 13:34:56.142 GCD[3726:150720] 任務3
2016-12-04 13:34:56.143 GCD[3726:150765] 任務4

1最外層分析,首先執行任務1,然後遇到非同步執行緒,不阻塞,直接任務5,由於非同步執行緒有任務2,直接輸出

2.這個非同步執行緒是全域性併發佇列,但是裡面又遇到了同步執行緒,也就是說任務2執行完之後執行緒阻塞,這個同步執行緒的任務3是加到mainQueue中的,也就是任務5之後

3.前面已經執行完了任務125或152,那麼阻塞的3可以順利執行,執行完3之後就可以順利地執行任務4

這裡寫圖片描述

執行緒死鎖4

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   NSLog(@"任務1");
   dispatch_sync(dispatch_get_main_queue(), ^{
       NSLog(@"任務2");
   });
   NSLog(@"任務3");
   });
NSLog(@"任務4");
while (1) {

}
NSLog(@"任務5");
這裡會有警告
code will never be executed 埋了隱患,編譯器還是還強的,注意看就能避免很多死鎖

列印日誌:

2016-12-04 13:52:09.597 GCD[3976:163387] 任務1
2016-12-04 13:52:09.597 GCD[3976:163302] 任務4    

分析:
1.一開始就是一個非同步執行緒,任務4,死迴圈和任務5,這裡註定了任務5不會被執行,如果其他執行緒有任務加到主執行緒中來,那麼必定卡死

2.肯定能列印1和4,然後非同步執行緒中遇到同步執行緒,同步執行緒的任務是加到mainQueue中的,也就是加到任務5之後,我擦,這肯定炸了,任務5是不會執行的,因此,任務3肯定不會被執行,而且非同步執行緒裡面的是同步阻塞的,那麼任務3之後的程式碼肯定也不會執行

3.這裡main裡面的死迴圈理論上是不會影響非同步執行緒中的任務1,2,3的,但是任務2是要被加到主佇列執行的,那麼憂鬱FIFO的原理,導致不會執行任務2,那麼就死鎖了

這裡寫圖片描述

總結

很多死鎖造成的原因第一點是在主執行緒或者子執行緒中遇到了一個同步執行緒,如果這個同步執行緒把任務加到自己所線上程的同步佇列裡面就會死鎖(mainQueue也是同步佇列)

dispatch_sync(來一個同一個同步佇列或者mainQueue, <#^(void)block#>)

這種情況下及其容易死鎖,千萬要小心

能明白就能讓上面的執行緒死鎖例子二進行解鎖

死鎖例子二:
解鎖1 新增一個序列佇列

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.mkj.serialQueue1", DISPATCH_QUEUE_SERIAL);

NSLog(@"任務1");
dispatch_async(serialQueue, ^{
    NSLog(@"任務2");
    dispatch_sync(serialQueue1, ^{
        NSLog(@"任務3");
    });
    NSLog(@"任務4");
});
NSLog(@"任務5");

解鎖2 用全域性併發佇列

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);

NSLog(@"任務1");
dispatch_async(serialQueue, ^{
    NSLog(@"任務2");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務3");
    });
    NSLog(@"任務4");
});
NSLog(@"任務5");

都能正常列印出

15234 或者 12345這個非同步的2和5無法確定,所有有幾種可能

總結下:
1.死鎖的情況,當前執行緒無論是主執行緒還是子執行緒,只要是序列佇列,我們繼續sync同步等待,然後加入block任務,這個時候就會死鎖,如上圖
2.同步併發佇列,由於同步的限制,只會在當前執行緒執行,因此併發和序列佇列都是一樣的
3.當我們async回到getmainqueue的時候,實際上是在主執行緒佇列最後追加任務,檔主執行緒其他任務完成後才回去執行回撥的block
4.案例死鎖第一個,當主執行緒呼叫sync同步等待任務,而且繼續加入到主執行緒中的時候,就會死鎖,一種解鎖辦法就是把sync的佇列換成自定義序列佇列或者併發佇列即可
5.按我現在的理解,開不開執行緒取決於同步還是非同步,同步不開,非同步開,開幾條取決於佇列,序列佇列開一條,併發佇列開多條(取決於cpu)
關於同步非同步:

6.dispatch_sync是同步函式,不具備開啟新執行緒的能力,交給它的block,只會在當前執行緒執行,不論你傳入的是序列佇列還是併發佇列,並且,它一定會等待block被執行完畢才返回。
dispatch_async是非同步函式,具備開啟新執行緒的能力,但是不一定會開啟新執行緒(例如async……get_main_queue就不會開執行緒,其他序列開一條,併發佇列開多條),交給它的block,可能在任何執行緒執行,開發者無法控制,是GCD底層在控制。它會立即返回,不會等待block被執行。
注意:以上兩個知識點,有例外,那就是當你傳入的是主佇列,那兩個函式都一定會安排block在主執行緒執行。記住,主佇列是最特殊的佇列

7.以上都是最簡單的理解,任務都是同步任務,那麼衍生出來一個超級大問題,如果Block裡面的任務是非同步網路請求,如何控制先後順序?如果Block任務裡面還巢狀非同步任務,因為併發佇列裡面的任務,只是負責列印和傳送請求的操作,非同步回撥資料是不歸佇列管的。
一道阿里的面試題
使用GCD如何實現A,B,C三個任務併發,完成後執行任務D?
不是讓你列印同步任務,而且網路併發任務的先後依賴如何形成?
另開一篇深入介紹

相關文章