iOS 多線程--GCD 串行隊列、併發隊列以及同步執行、異步執行

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 歲的小孩去介紹併發和並行的區別

15248975776264.jpg

 

同步和異步

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(@"");之後就按順序開始了,爲什麼?如果你知道,請不吝賜教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章