逛到 stackoverflow 上面一篇提問,引起了我的深思,寫了這麼久的代碼,反過頭來去看最最基本的東西,卻又覺得十分有趣。不廢話了,直接上鍊接:
https://stackoverflow.com/questions/19180661/sync-dispatch-on-current-queue
引起我興趣的主要是最下面的回覆,我截圖下來了,不能連外網的朋友可以看一下:
可以看到,上面有四個示例,其中的兩個死鎖了,另外兩個正常運行。先來普及一下死鎖的概念再解釋原因。
死鎖:在一個串行隊列Q中執行任務A,可是此時又要在任務A中同步執行任務B,將任務B也是添加到了串行隊列Q中,這樣就會發生死鎖。
就如同上面兩個死鎖的情況,外層 block 要等待執行完畢後纔算完成,而此時在外層 block 內部又加了一個任務,而這個任務強制要同步執行,強制插入,這樣一來就必須先執行內部的任務,可以此時線程隊列被外層 block 佔用着,必須等待外層 block 走到頭纔可以走新添加的任務。這樣兩個任務牽制彼此,都無法完成。
那麼我們改一下 Situation 3 中的代碼:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self goDoSomethingLongAndInvolved];
dispatch_sync(queue, ^{
NSLog(@"Situation 3");
});
});
將外層的隊列改爲併發隊列,那麼結果就是可以執行了。首先要明白的是這個外層的併發隊列中只有一個任務,所以不存在什麼串行、併發之分。又因爲沒有新的線程,所以內部先執行 goDoSome... 方法,再執行 NSLog 輸出,最後執行外層 block 結束花括號。而內層的同步也就說明了必須先執行內層 block 內的代碼,再繼續向下執行。因爲分屬不同的隊列,所以就讓線程先過去執行內層 block,再回過頭來執行外層的 block 剩下部分收尾就可以了。
打個比方:一個工人(一條線程)去完成兩條生產線(兩個隊列)的任務,正在生產線A(外層 block)工作,突然被叫到生產線B(內層 block)去工作,而且是必須去(同步執行),做完生產線B的任務,接着回來收尾生產線A的任務(收尾最後外層 block 的花括號)。
那 Situation 1 爲什麼沒有發生死鎖呢?答:因爲內層的 block 是異步執行的,也就是在單線程並且同一串行隊列中,內層 block 可以等待串行隊列中其餘的任務完成再來執行內層 block 內的任務,即等待外層 block 右花括號結束之後再來執行 NSLog。
Situation 4 同理。
Situation 1 和 Situation 4 的區別在於外層 block 是異步調用還是同步調用,但異步和同步是相對於同一線程下同級的任務而言的,所以說這裏的任務只存在外層的 block 一個,所以說異步同步就沒有區別了。
寫了幾個個小 Demo,大家可以分析一下。
第一個 demo,初步理解:
兩個 VC,YellowVC push 到 GreenVC,在 GreenVC 中添加一個 pop 按鈕。按鈕的點擊方法實現如下:
- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任務A
sleep(3); // 任務B
NSLog(@"C"); // 任務C
};
[self.navigationController popViewControllerAnimated:true]; // 任務D
dispatch_async(dispatch_get_main_queue(), ^{
self.myBlock();
});
}
那麼問題是任務 A、B、C、D執行順序如何?(正解:D A B C)
那麼我們再修改一下代碼:
- (IBAction)btnClick2:(UIButton *)sender {
self.myBlock = ^{
NSLog(@"A"); // 任務A
sleep(3); // 任務B
NSLog(@"C"); // 任務C
};
[self.navigationController popViewControllerAnimated:true]; // 任務D
self.myBlock();
}
那麼問題又來了,現在的任務調用順序是什麼呢?(正解:A B C D)
理由:上面兩段代碼,其區別在於 block 是否是異步調用的。而我們理解這兩段程序首先要明白一點,那就是 pop 方法是異步調用的。如果 pop 不是異步調用的,那麼你怎麼可能在 pop 執行過後再去拿到 self 指針去調用其他的方法呢?pop 過後,self 可就出棧了,那樣一來在 pop 之後調用 self 指針不就會報錯了嗎。
所以說第一段代碼 pop 和 block 都是異步調用的,所以按照順序執行。主線程中原本包含任務 btnClick2,所以按照順序執行完 btnClick2 就可以了,而 pop 也不會受到影響(這裏指卡頓)。第二段代碼由於 pop 異步,並且主線程會將 pop 異步放在最後執行。那麼主線程在執行 btnClick2 這個任務時,就會先執行完 myBlock 的賦值,然後將 pop 異步放在最後調用,再接着執行 myBlock 的代碼,再接着執行 btnClick2 的右花括號。這樣一來 btnClick2 任務就完全執行完畢,主線程空閒,此時再執行 pop 任務。所以說運行結果會先打印A,然後卡頓3s,然後打印C,最後再 pop。此時會造成 pop 的卡頓。
第二個 demo,有助於加深理解:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
那麼上面的這 5 句打印,是以什麼順序執行的?
我們先爲這段代碼加上註釋:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行隊列
// 主線程執行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
// 添加到不同隊列的任務,async 可以開線程立即執行,主線程不等待此任務執行完
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
// 主隊列任務,async 不開線程,任務加入到主隊列後面,主線程不等待此任務執行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主線程執行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
然後再來分析結果:
由於在主線程中順序執行,所以 1 - 2 是首要先執行的,然後 3 會開線程並且啓用全新的串行隊列,所以 3 立即執行,但 3 開線程需要耗時。由於 4 主線程主隊列異步等待執行,而 5 是主線程主隊列正常順序執行,所以 5 會在 4 之前執行。
所以得出結論:
1 2 肯定先順序打印,3 4 5 均在 1 2 後面打印,4 一定在 5 之後打印,3 不一定在 1 2 之後的哪個位置打印。
第三個 demo,相信這個 demo 過後你就會對 異步/同步/串行/併發 理解的更加透徹了:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil);
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
那麼上面的這 8 句打印,是以什麼順序執行的?
我們還是先爲這段代碼加上註釋:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); // 串行隊列
// 主線程執行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
// 主線程等待sync完成任務再繼續執行70行往下的代碼。由於GCD優化,sync的任務會被當前線程(主線程)執行。
dispatch_sync(queue, ^{
NSLog(@"doSomething 2.5 %@", [NSThread currentThread]);
});
// 添加到不同隊列的任務,async可以開線程立即執行,主線程不等待此任務執行完
dispatch_async(queue, ^{
// =====此時是子線程在執行代碼=====
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
// 添加到同一個串行隊列的異步任務,不開線程,任務加入到串行隊列後面,當前線程不等待任務執行完
dispatch_async(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
// 當前線程先完成當前任務
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
// 主隊列任務,async不開線程,任務加入到主隊列後面,主線程不等待此任務執行完
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
});
// 主線程執行
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
}
我們直接得出結果:
1 2 2.5 肯定率先執行,(3 6 7)在新的線程中執行,(3 6 7)整體順序未知,5 肯定在 4 的前面執行。(3 6 7)內部肯定是 3 7 6 的順序執行。
然後我們將 6 的地方改爲:
dispatch_sync(queue, ^{
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
那麼結果會是怎樣的?
直接得出結果:
主線程正常,而子線程死鎖,App 還是崩潰了。
第四個 demo,如果你分析對了調用順序,基本上這一部分就理解了:
- (IBAction)btnClick2:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("darktest", nil); //串行隊列
dispatch_queue_t queue2 = dispatch_queue_create("darktest2", nil); //串行隊列2
//主線程執行
NSLog(@"doSomething 1 %@", [NSThread currentThread]);
dispatch_sync(queue, ^{ // A
NSLog(@"doSomething 2 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // B
NSLog(@"doSomething 3 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{ // C
NSLog(@"doSomething 4 %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{ // D
NSLog(@"doSomething 5 %@", [NSThread currentThread]);
dispatch_sync(queue2, ^{ // E
NSLog(@"doSomething 6 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 7 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 8 %@", [NSThread currentThread]);
});
NSLog(@"doSomething 9 %@", [NSThread currentThread]);
});
//主線程執行
NSLog(@"doSomething 10 %@", [NSThread currentThread]);
}
直接給出答案:1 2 3 9 (4) 10 (4) 5 6 7 8
其中 4 的打印位置不確定,如果你也得到的這個結果,那麼恭喜你。在這裏不做解釋。不明白的可以留言。
最後得出我們本文的結論:
異步、同步、串行、併發 這些名詞的概念都不能脫離 線程、隊列 來單獨描述。單純去解釋一個名詞在我看來是沒有意義的。大家通過 demo 可以理解了就好。
本文感謝:darkhandz 提供 demo,感謝我倆兩天如同漿糊般的討論。