1. 程式人生 > >關於GCD執行緒死鎖的一點兒理解

關於GCD執行緒死鎖的一點兒理解

上一篇在總結 GCD 的時候讀到了一篇部落格,提到了這麼一個問題:既然在主佇列(dispatch_get_main_queue)中同步(dispatch_sync())執行一個任務會造成死鎖,

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync----%@",[NSThread currentThread]);
    });
    NSLog(@"%@",[NSThread currentThread]);
}

為什麼在一個全域性佇列中同步執行任務不會造成死鎖?

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"sync----%@",[NSThread currentThread]);
    });
    NSLog(@"%@",[NSThread currentThread]);
}

原文是這麼寫的“仔細分析一下:主執行緒先執行第一句列印,然後執行dispatch_sync方法,往全域性佇列中新增一個同步任務,此時主執行緒應該阻塞住,等待全域性佇列中同步任務的執行完畢,而全域性佇列dispatch_sync方法並不會建立新執行緒,那肯定需要在主執行緒執行(上一篇的結論),可是主執行緒已經阻塞了,所以按理說此時應該發生死鎖,可是為什麼從列印結果看它確實是在主執行緒執行的同步任務,而且並沒有發生死鎖現象呢?”,如果不深入分析,貌似說的也不錯,主執行緒已經阻塞了,肯定會造成死鎖,但是為什麼沒造成死鎖呢?

其實稍微深入分析一下,就會發現錯的有多離譜。如果按照這樣的邏輯,同步任務一旦阻塞了主執行緒,就會造成死鎖,那麼OC就沒得玩了。因為在OC的程式中,主執行緒是一直都存在的,一直在迴圈著處理各種任務,除非是關閉了應用。

而這裡之所以沒有造成死鎖,是因為同步任務阻塞的並不是當前執行緒(在這個例子中就是主執行緒),而是阻塞了當前的佇列。舉個例子就很明白了。

// 序列佇列同步執行
-(void)serialQueueSync{
    //建立序列佇列
    dispatch_queue_t queue = dispatch_queue_create("firstSerialQueue", DISPATCH_QUEUE_SERIAL);
    NSLog(@"現在開始執行了--%@",[NSThread currentThread]);
    
    //新增兩個任務到佇列中 同步執行
    dispatch_sync(queue, ^{
        NSLog(@"+++++++%@",[NSThread currentThread]);
    });
    NSLog(@"列印+結束");
    
    dispatch_sync(queue, ^{
        NSLog(@"_______%@",[NSThread currentThread]);
    });
    NSLog(@"列印-結束");
    
}
2018-03-17 18:37:14.199 3[4786:495520] 現在開始執行了--<NSThread: 0x60000006b080>{number = 1, name = main}
2018-03-17 18:37:14.199 3[4786:495520] +++++++<NSThread: 0x60000006b080>{number = 1, name = main}
2018-03-17 18:37:14.199 3[4786:495520] 列印+結束
2018-03-17 18:37:14.200 3[4786:495520] _______<NSThread: 0x60000006b080>{number = 1, name = main}
2018-03-17 18:37:14.200 3[4786:495520] 列印-結束

從列印結果中就可以很明顯的看出來,主執行緒在列印完第一句話之後遇到了同步任務,並沒有被同步任務阻塞,而是去序列佇列中(firstSerialQueue)執行任務去了,在序列佇列中的同步任務完成後又回到了被阻塞的主佇列中繼續執行主佇列中的任務。所以說同步任務並不會阻塞執行緒(不管是主執行緒還是子執行緒)。雖然主佇列和firstSerialQueue都是序列佇列,但因為他們不是同一個佇列,所以不會造成執行緒死鎖。

也許現在你會問,那為什麼在主佇列中執行同步任務會造成執行緒死鎖呢?現在我們再來看看那段程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",[NSThread currentThread]);//任務1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync----%@",[NSThread currentThread]);//任務2
    });
    NSLog(@"%@",[NSThread currentThread]);//任務3
}

從程式碼中可以看出,這個同步任務是把任務加到了主佇列中,也就是加到被自己阻塞的佇列中了,而恰好這個主佇列又是一個序列佇列,序列佇列遵循著‘先進先出’的原則,所以才會造成死鎖的。具體來講就是一個矛與盾的問題,因為列印完任務1之後,遇到的同步任務把任務2加到了主佇列的後面,也就是任務3的後面,根據序列佇列‘先進先出’的原則,肯定是要先執行任務3再執行任務2,就是1->3->2的執行順序。而同步任務又要求當前執行緒必須在執行完同步任務也就是任務2之後再回去執行任務3,是1->2->3的順序,所以這個時候主執行緒就傻掉了,主執行緒此時內心是這樣想的“我是誰?我從哪裡來的?我要去幹什麼?”,所以就掛掉了。這個跟上面的序列佇列同步執行的例子不同的地方在於,同步任務是在同一個序列佇列中,所以會死鎖。

聰明的你也許會立馬發現另外一個問題,如果當前佇列不是序列佇列,會不會造成執行緒死鎖?那我們稍微改一下程式碼試試:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_queue_create("firstConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
        NSLog(@"%@",[NSThread currentThread]);//任務1
        dispatch_sync(queue, ^{
            NSLog(@"sync----%@",[NSThread currentThread]);//任務2
        });
        NSLog(@"%@",[NSThread currentThread]);//任務3
    }); 
}
2018-03-17 19:12:28.236 3[5311:565511] <NSThread: 0x60800007d300>{number = 1, name = main}
2018-03-17 19:12:28.237 3[5311:565511] sync----<NSThread: 0x60800007d300>{number = 1, name = main}
2018-03-17 19:12:28.237 3[5311:565511] <NSThread: 0x60800007d300>{number = 1, name = main}

這次是把同樣的任務放到了並行佇列中,但從列印結果中可以看出,並沒有造成死鎖,這是因為並行佇列不要求‘先進先出’,既然同步任務要求先執行任務2,那就先執行任務2好了。

寫的有點兒囉嗦了,如果有不對的地方還請大牛指出,萬分感謝!(好了,週六加班到現在,我也該去吃飯了大笑)。