iOS 多執行緒--GCD 序列佇列、併發佇列以及同步執行、非同步執行

蛋蛋_發表於2018-05-04

1 什麼是佇列(queue)

在開始GCD之前先來說一下佇列的概念,因為GCD的任務都是在佇列中派發的; 佇列(queue):是先進先出(FIFO, First-In-First-Out)的線性表。但是在佇列前面加上序列併發這兩個定語之後,也就是序列佇列併發佇列,有時就容易搞不清楚了,特別是再加上同步非同步的概念之後,有時就更不清楚了。

2 序列佇列和併發佇列

注意是併發佇列(Concurrent Queue),不是並行佇列,關於併發並行的區別見下一節

什麼是序列佇列併發佇列 呢?上面已經說了序列佇列併發佇列中的序列併發佇列的定語,可以加個序列的佇列併發的佇列;所以序列佇列並行佇列說到底還是佇列,既然是佇列,肯定是要先進先出(FIFO, First-In-First-Out)的,記住這一點很重要。

序列佇列:說明這個佇列中的任務要序列執行,也就是一個一個的執行,必須等上一個任務執行完成之後才能開始下一個,而且一定是按照先進先出的順序執行的,比如序列佇列裡面有4個任務,進入佇列的順序是a、b、c、d,那麼一定是先執行a,並且等任務a完成之後,再執行b... 。

併發佇列:說明這個佇列中的任務可以併發執行,也就任務可以同時執行,比如併發佇列裡面有4個任務,進入佇列的順序是a、b、c、d,那麼一定是先執行a,再執行b...,但是執行b的時候a不一定執行完成,而且a和b具體哪個先執行完成是不確定的, 具體同時執行幾個,由系統控制(GCD中不能直接設定併發數,可以通過建立訊號量的方式實現,NSOperationQueue可以直接設定),但是肯定也是按照先進先出(FIFO, First-In-First-Out)的原則呼叫的。

4 關於併發並行

並行的英文是parallelism,併發的英文時concurrency ,

  1. 並發表示邏輯概念上的同時,並行表示物理概念上的同時。

  2. 併發指的是程式碼的性質,並行指的是物理執行狀態

  3. 併發是說程式B的開始時間是在程式A的開始時間與結束時間之間,我們就說A和B是併發的。並行指同一時間兩個執行緒執行在不同的cpu。

  4. 併發是同時處理很多事情(dealing with lots of things at once),並行是同時執行很多事情(doing lots of things at once);

  5. 併發可認為是一種邏輯結構的設計模式。你可以用併發的設計方式去編寫程式,然後執行在一個單核cpu上,通過cpu動態地邏輯切換製造出並行的假象。此時,你的程式不是並行,但是是併發的。如果將併發的程式執行在多核CPU上,此時你的程式可以認為是並行。並行更關注的是程式的執行(execution);

  6. 對於單核CPU來說,並行至少兩個CPU才行;而併發一個cpu也可以,兩個任務交替執行即可;

綜上所述:併發更多的是編寫程式上的概念,並行是物理CPU執行上的概念。併發可以用並行的方式實現。併發是從程式設計的角度來解釋的,並行是從cpu執行任務的角度來看的,一般來說我們只能編寫併發的程式,卻無法保證編寫出並行的程式。

可以把併發和並行當成不同維度的東西。併發是從程式設計師編寫程式的角度來看的。並行是從程式的物理執行上來看的。

Erlang 的發明者 Joe Armstrong 在他的一篇博文 (http://joearms.github.io/2013/04/05/concurrent-and-parallel-programming.html) 中提到如何向一個 5 歲的小孩去介紹併發和並行的區別

iOS 多執行緒--GCD 序列佇列、併發佇列以及同步執行、非同步執行

同步和非同步

GCD中的同步非同步是針對任務的執行來說的,也就是同步執行任務和非同步執行任務。 同步或非同步描述的是task與其上下文之間的關係

同步執行:可以理解為,呼叫函式時(或執行一個程式碼塊時),必須等這個函式(或程式碼塊)執行完成之後才會執行下面的程式碼。 同步執行 一般在當前執行緒中執行任務,不會開啟新的執行緒。

非同步:不管呼叫的函式有沒有執行完,都會繼續執行下面的程式碼。具備開啟新執行緒的能力。

同步和非同步的主要區別是向佇列裡面新增任務時是立即返回還是等新增的任務完成之後再返回。

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
複製程式碼

dispatch_sync就是新增同步任務的,新增任務的時候,必須等block裡面的程式碼執行完,dispatch_sync這個函式才能返回。

dispatch_async是新增非同步任務的,新增任務的時候會立即返回,不管block裡面的程式碼是否執行。

測試

  1. 序列佇列非同步任務 以下程式碼均是在viewDidLoad方法中執行的
dispatch_queue_t serialQueue = dispatch_queue_create("Dan-serial", DISPATCH_QUEUE_SERIAL);
    for(int i = 0; i < 5; i++){
        dispatch_async(serialQueue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
            [NSThread sleepForTimeInterval: i % 3];
        });
    }
輸出如下:    
我開始了:0 , <NSThread: 0x60400027e480>{number = 3, name = (null)}
我開始了:1 , <NSThread: 0x60400027e480>{number = 3, name = (null)}
我開始了:2 , <NSThread: 0x60400027e480>{number = 3, name = (null)}
我開始了:3 , <NSThread: 0x60400027e480>{number = 3, name = (null)}
我開始了:4 , <NSThread: 0x60400027e480>{number = 3, name = (null)}
複製程式碼

可以看到是按順輸出的,是在同一個執行緒,而且開啟了新執行緒,

  1. 序列佇列同步任務
for(int i = 0; i < 5; i++){
        dispatch_sync(serialQueue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
            [NSThread sleepForTimeInterval: i % 3];
        });
    }  
輸出如下:    
我開始了:0 , <NSThread: 0x60000006d8c0>{number = 1, name = main}
我開始了:1 , <NSThread: 0x60000006d8c0>{number = 1, name = main}
我開始了:2 , <NSThread: 0x60000006d8c0>{number = 1, name = main}
我開始了:3 , <NSThread: 0x60000006d8c0>{number = 1, name = main}
我開始了:4 , <NSThread: 0x60000006d8c0>{number = 1, name = main}
複製程式碼

可以看到是按順輸出的,是在同一個執行緒,但是沒有開啟新執行緒,是在主執行緒執行的

  1. 併發佇列非同步任務
    dispatch_queue_t concurrent_queue = dispatch_queue_create("DanCONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    for(int i = 0; i < 5; i++){
        dispatch_async(concurrent_queue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
            [NSThread sleepForTimeInterval: i % 3];
            NSLog(@"執行完成:%@ , %@",@(i),[NSThread currentThread]);
        });
    }
輸出如下:
我開始了:0 , <NSThread: 0x600000462340>{number = 3, name = (null)}
我開始了:2 , <NSThread: 0x604000269380>{number = 6, name = (null)}
我開始了:3 , <NSThread: 0x604000269180>{number = 5, name = (null)}
我開始了:1 , <NSThread: 0x600000461d80>{number = 4, name = (null)}
執行完成:0 , <NSThread: 0x600000462340>{number = 3, name = (null)}
執行完成:3 , <NSThread: 0x604000269180>{number = 5, name = (null)}
我開始了:4 , <NSThread: 0x600000462340>{number = 3, name = (null)}
執行完成:1 , <NSThread: 0x600000461d80>{number = 4, name = (null)}
執行完成:4 , <NSThread: 0x600000462340>{number = 3, name = (null)}
執行完成:2 , <NSThread: 0x604000269380>{number = 6, name = (null)}
複製程式碼

可以看到,是併發執行的,而且開啟了不止一個新執行緒。 這裡有沒有發現什麼不對的地方呢?執行完成的順序不確定是可以理解的,但是開始的順序為什麼也不確定呢?根據上面說的,佇列是先進先出的,那麼我開始了應該按照順序列印才對,但是實際列印是無序的,為什麼?這個問題暫時還沒搞清楚,我的猜測可能是NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);這個操作比較耗時導致的。

  1. 併發佇列同步任務
 for(int i = 0; i < 5; i++){
        dispatch_sync(concurrent_queue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
            [NSThread sleepForTimeInterval: i % 3];
            NSLog(@"執行完成:%@ , %@",@(i),[NSThread currentThread]);
        });
    }
    
輸出如下:
我開始了:0 , <NSThread: 0x60000007ec80>{number = 1, name = main}
執行完成:0 , <NSThread: 0x60000007ec80>{number = 1, name = main}
我開始了:1 , <NSThread: 0x60000007ec80>{number = 1, name = main}
執行完成:1 , <NSThread: 0x60000007ec80>{number = 1, name = main}
我開始了:2 , <NSThread: 0x60000007ec80>{number = 1, name = main}
執行完成:2 , <NSThread: 0x60000007ec80>{number = 1, name = main}
我開始了:3 , <NSThread: 0x60000007ec80>{number = 1, name = main}
執行完成:3 , <NSThread: 0x60000007ec80>{number = 1, name = main}
我開始了:4 , <NSThread: 0x60000007ec80>{number = 1, name = main}
執行完成:4 , <NSThread: 0x60000007ec80>{number = 1, name = main}
複製程式碼

可以看到,程式沒有併發執行,而且沒有開啟新執行緒,是在主執行緒執行的。 有沒有覺得奇怪呢?為什麼向併發佇列新增的任務,沒有開啟新執行緒,而是在主執行緒執行的? 如下解釋:

使用dispatch_sync 新增同步任務,必須等新增的block執行完成之後才返回。
既然要執行block,肯定需要執行緒,要麼新開執行緒執行,要麼再已存在的執行緒(包括當前執行緒)執行。  
dispatch_sync的官方註釋裡面有這麼一句話:
As an optimization, dispatch_sync() invokes the block on the current thread when possible.
作為優化,如果可能,直接在當前執行緒呼叫這個block。
  
所以,一般,在大多數情況下,通過dispatch_sync新增的任務,在哪個執行緒新增就會在哪個執行緒執行。

上面我們新增的任務的程式碼是在主執行緒,所以就直接在主執行緒執行了。

複製程式碼

序列佇列裡的任務都在一個執行緒上執行?

測試如下

- (void)viewDidLoad {
    dispatch_queue_t serialQueue = dispatch_queue_create("Dan-serial", DISPATCH_QUEUE_SERIAL);

  dispatch_sync(serialQueue, ^{
      // block 1
      NSLog(@"current 1: %@", [NSThread currentThread]);
  });

  dispatch_sync(serialQueue, ^{
      // block 2
      NSLog(@"current 2: %@", [NSThread currentThread]);
  });

  dispatch_async(serialQueue, ^{
      // block 3
      NSLog(@"current 3: %@", [NSThread currentThread]);
  });

  dispatch_async(serialQueue, ^{
      // block 4
      NSLog(@"current 4: %@", [NSThread currentThread]);
  });
}
  //結果如下
  //    current 1: <NSThread: 0x600000071600>{number = 1, name = main}
//    current 2: <NSThread: 0x600000071600>{number = 1, name = main}
//    current 3: <NSThread: 0x60400027bcc0>{number = 3, name = (null)}
//    current 4: <NSThread: 0x60400027bcc0>{number = 3, name = (null)}
複製程式碼

可以看到,向序列佇列新增的同步任務在主執行緒執行的,和上面的結論一致(通過dispatch_sync新增的任務,在哪個執行緒新增就會在哪個執行緒執行)。 非同步任務在新開的執行緒執行的,而且只開了一個執行緒

再做如下測試:

- (void)viewDidLoad {
    dispatch_queue_t queue = dispatch_queue_create("Dan", NULL);
     dispatch_async(queue, ^{
      NSLog(@"current : %@", [NSThread currentThread]);
      dispatch_queue_t serialQueue = dispatch_queue_create("Dan-serial", DISPATCH_QUEUE_SERIAL);

      dispatch_sync(serialQueue, ^{
          // block 1
          NSLog(@"current 1: %@", [NSThread currentThread]);
      });

      dispatch_sync(serialQueue, ^{
          // block 2
          NSLog(@"current 2: %@", [NSThread currentThread]);
      });

      dispatch_async(serialQueue, ^{
          // block 3
          NSLog(@"current 3: %@", [NSThread currentThread]);
      });

      dispatch_async(serialQueue, ^{
          // block 4
          NSLog(@"current 4: %@", [NSThread currentThread]);
      });
  });
}
// 結果如下
//    current  : <NSThread: 0x604000263440>{number = 3, name = (null)}
//    current 1: <NSThread: 0x604000263440>{number = 3, name = (null)}
//    current 2: <NSThread: 0x604000263440>{number = 3, name = (null)}
//    current 3: <NSThread: 0x604000263440>{number = 3, name = (null)}
//    current 4: <NSThread: 0x604000263440>{number = 3, name = (null)}

複製程式碼

可以看到:

  • 在主執行緒向自定義的序列佇列新增的同步任務,直接在主執行緒執行

  • 在主執行緒向自定義的序列佇列新增的非同步任務,會開一個新執行緒

  • 在非主執行緒向自定義的序列佇列新增的同步任務,直接在當期執行緒執行

  • 在非主執行緒向自定義的序列佇列新增的非同步任務,直接在當期執行緒執行

結論:使用dispatch_sync函式新增到serial dispatch queue中的任務,其執行的task往往與所在的上下文是同一個thread;使用dispatch_async函式新增到serial dispatch queue中的任務,一般會(不一定)新開一個執行緒,但是不同的非同步任務用的是同一個執行緒。

測試:

  1. 主執行緒只會執行主佇列的任務? 不是的,如上

  2. 以下程式碼的執行結果是什麼?為什麼?

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_queue_create("com.dan.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"current thread = %@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"current thread = %@", [NSThread currentThread]);
        });
    });    
}
複製程式碼

輸出current thread = <NSThread: 0x60000006d600>{number = 1, name = main},然後發生死鎖。 原因:使用dispatch_sync向序列佇列新增任務,會在當前執行緒執行,而當前執行緒就是主線執行緒,所以第一個NSLog輸出,由於第一個dispatch_sync的 block程式碼是在主執行緒執行的,所以第二個dispatch_sync相當於如下寫法,所以會發生死鎖,如果不明白為什麼,找Google。

- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"current thread = %@", [NSThread currentThread]);
        });
}
複製程式碼

會發生死鎖,

疑問

- (void)viewDidLoad {
    dispatch_queue_t concurrent_queue = dispatch_queue_create("Dan——CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    for(int i = 0; i < 10; i++){
        dispatch_async(concurrent_queue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
        });
    }
}
//執行結果如下
//我開始了:3 , <NSThread: 0x60000026e240>{number = 6, name = (null)}
//我開始了:1 , <NSThread: 0x60400027a440>{number = 4, name = (null)}
//我開始了:2 , <NSThread: 0x60000026f800>{number = 5, name = (null)}
//我開始了:0 , <NSThread: 0x60400027a400>{number = 3, name = (null)}
//我開始了:4 , <NSThread: 0x60000026e240>{number = 6, name = (null)}
//我開始了:5 , <NSThread: 0x60400027a440>{number = 4, name = (null)}
//我開始了:8 , <NSThread: 0x60000026e980>{number = 9, name = (null)}
//我開始了:7 , <NSThread: 0x60000026e800>{number = 8, name = (null)}
//我開始了:6 , <NSThread: 0x60000026e8c0>{number = 7, name = (null)}
//我開始了:9 , <NSThread: 0x60400027a280>{number = 10, name = (null)}
複製程式碼

為什麼不按順序開始?併發佇列也是佇列,佇列應該是先進先出,雖然執行結束的順序不確定,但是開始的時候應該是確定的啊

- (void)viewDidLoad {
    dispatch_queue_t concurrent_queue = dispatch_queue_create("Dan——CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    for(int i = 0; i < 10; i++){
        dispatch_async(concurrent_queue, ^{
            NSLog(@"我開始了:%@ , %@",@(i),[NSThread currentThread]);
        });
    }
    NSLog(@"");
}
//執行結果如下
//我開始了:0 , <NSThread: 0x60000027d200>{number = 3, name = (null)}
//我開始了:1 , <NSThread: 0x60000027d740>{number = 4, name = (null)}
//我開始了:2 , <NSThread: 0x60000027d200>{number = 3, name = (null)}
//我開始了:3 , <NSThread: 0x60000027d740>{number = 4, name = (null)}
//我開始了:4 , <NSThread: 0x60000027d200>{number = 3, name = (null)}
//我開始了:5 , <NSThread: 0x60000027d740>{number = 4, name = (null)}
//我開始了:6 , <NSThread: 0x60000027d200>{number = 3, name = (null)}
//我開始了:7 , <NSThread: 0x60000027d740>{number = 4, name = (null)}
//我開始了:8 , <NSThread: 0x60000027d200>{number = 3, name = (null)}
//我開始了:9 , <NSThread: 0x60000027d740>{number = 4, name = (null)}
複製程式碼

加了NSLog(@"");之後就按順序開始了,為什麼?如果你知道,請不吝賜教。

相關文章