iOS 【一篇文章引發的思考 —— 異步/同步/併發/串行】

逛到 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,感謝我倆兩天如同漿糊般的討論。

發佈了262 篇原創文章 · 獲贊 817 · 訪問量 61萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章