iOS多執行緒開發—GCD (一)

Oyster發表於2019-03-03

GCD是什麼?

作為一個iOS開發者,無論你是大神還是像我這樣的菜鳥,每一個人應該都不會對多執行緒開發陌生,即便你沒有聽說過pthread,NSThread,NSOperation,但你至少多少聽說過或者使用過這樣的程式碼

dispatch_async(dispatch_get_main_queue,{
  //在這裡搞事情
});
複製程式碼

那麼恭喜你,你會GCD!
其實當我第一次使用這個程式碼的時候,我並不確切的理解以上這段程式碼幹了什麼,我只知道這樣幹不會讓我的介面處於沒有反應的狀態。隨著開發經驗的累積,越來越多的使用了有關多執行緒的知識,因此在這裡把我的一些淺薄的理解記錄下來,幫助自己,也希望能夠幫助到其他需要的人。

在這裡我們先給GCD做個定義吧:
1.GCD是Grand Central Dispatch的縮寫,中文可以稱為巨牛X的中央派發。這其實是蘋果公司為我們提供的一種多執行緒程式設計API。
2.GCD通過block讓我們可以很容易的將要執行的任務放入佇列中,我們不需要關心任務在哪一個執行緒中執行,這就讓開發者能夠更容易的使用多執行緒技術進行程式設計而不用實際操作執行緒。
3.GCD為我們提供了建立佇列的方法,並且提供了任務同步和非同步執行的方法。我們所需要關心的只是如何去定義一個任務。

程式,執行緒,同步,非同步,並行,併發,序列?傻傻分不清?

我們常常說多執行緒程式設計,那麼究竟什麼是多執行緒程式設計,我們為什麼要使用多執行緒程式設計技術呢?
要搞清這麼多概念我們首先要簡單的說一下計算機的CPU!
現在的計算機多是所謂的多核計算機,在一個物理核心以外還會使用軟體技術創造出多個虛擬核心。但是無論是有多少個核心,一個CPU核心在同一時間內只能執行一條無分叉的程式碼指令這條無分叉的程式碼指令就是我們常說的執行緒(後面會給出執行緒更具體的定義),因此如果想提高計算機的執行效率,我們除了讓CPU具有更多核心以外,還需要使用多執行緒的方式,讓同一個核心在多條執行緒之間做切換以此來提高CPU的使用效率。

1.程式(process)

  • 程式是指在系統中正在執行的一個應用程式,就是一段程式的執行過程。
  • 每個程式之間是相互獨立的, 每個程式均執行在其專用且受保護的記憶體空間內。
  • 程式是一個具有一定獨立功能的程式關於某次資料集合的一次執行活動,它是作業系統分配資源的基本單元。
  • 程式狀態:程式有三個狀態,就緒,執行和阻塞。就緒狀態其實就是獲取了除cpu外的所有資源,只要處理器分配資源馬上就可以執行。執行態就是獲取了處理器分配的資源,程式開始執行,阻塞態,當程式條件不夠時,需要等待條件滿足時候才能執行,如等待I/O操作的時候,此刻的狀態就叫阻塞態。

2.執行緒(thread)

  • 一個程式要想執行任務,必須要有執行緒,至少有一條執行緒。
  • 一個程式的所有任務都是線上程中執行。
  • 每個應用程式想要跑起來,最少也要有一條執行緒存在,其實應用程式啟動的時候我們的系統就會預設幫我們的應用程式開啟一條執行緒,這條執行緒也叫做`主執行緒`,或者`UI執行緒`。
iOS多執行緒開發—GCD (一)

3.程式和執行緒的關係

  • 執行緒是程式的執行單元,程式的所有任務都線上程中執行!
  • 執行緒是 CPU 呼叫的最小單位。
  • 程式是 CPU 分配資源和排程的單位。
  • 一個程式可以對應過個程式,一個程式中可有多個執行緒,但至少要有一條執行緒。
  • 同一個程式內的執行緒共享程式資源。

有關程式和執行緒的內容我參考了《iOS開發之多執行緒程式設計總結(一)》,這篇文章對於這方面的總結很到位,有興趣的朋友可以看一下。
下面我談一下我自己對於同步,非同步,並行,併發,序列的理解。

4.同步(Synchronous) VS. 非同步(Asynchronous)
在這裡呢我也不談什麼理論了,就我開發過程中的理解談一下吧。

NSLog(@"Hellot, task1");
    dispatch_queue_t queue = dispatch_queue_create("Sereal_Queue_1", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        for (int i = 0; i < 10 ; i ++) {
            
            NSLog(@"%d",i);
        }
    });
    
NSLog(@"Hello, taks2");
dispatch_async(queue, ^{
        for (int i = 0; i < 10 ; i ++) {
            
            NSLog(@"%d",i);
        }

    });
NSLog(@"Hello,task3");
複製程式碼

列印結果如下

2017-07-01 20:40:56.914 GCD_Test[85651:1471470] Hellot, task1
2017-07-01 20:40:56.914 GCD_Test[85651:1471470] 0
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 1
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 2
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 3
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 4
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 5
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 6
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 7
2017-07-01 20:40:56.915 GCD_Test[85651:1471470] 8
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] 9
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] Hello, taks2
2017-07-01 20:40:56.916 GCD_Test[85651:1471470] Hello,task3
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 0
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 1
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 2
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 3
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 4
2017-07-01 20:40:56.916 GCD_Test[85651:1471599] 5
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 6
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 7
2017-07-01 20:40:56.917 GCD_Test[85651:1471599] 8
2017-07-01 20:40:56.922 GCD_Test[85651:1471599] 9
複製程式碼

從上面的列印結果我們可以看出,在hello,task1和hello,task2之間我們執行了一個同步任務,這個任務被放在了一個序列佇列當中。因此,Hello,task2必須要等待佇列中的任務被執行完畢之後才會執行。當我們在hello,task2和hello,task3中執行了非同步任務的時候,hello,task3不需要等待佇列中的任務被執行完再執行。
因此我們可以這樣認為,同步任務必須等到任務執行結束之後才會返回,非同步任務不需要等待任務結束可以立即返回執行下面的程式碼。

5.並行(Paralleism) VS. 併發(Concurrency)
其實簡單來說並行和併發都是計算機同時執行多個執行緒的策略。只是併發是一種”偽同時”,前面我們已經說過,一個CPU核心在同一個時間只能執行一個執行緒,併發是通過讓CPU核心在多個執行緒做快速切換來達到讓程式看起來是同時執行的目的。這種上下文切換(context-switch)的速度非常快,以此來達到執行緒看起來是並行執行的目的。而並行是多條執行緒在多個CPU核心之間同時執行,以此來達到提高執行效率的目的。

iOS多執行緒開發—GCD (一)

6.並行(Paralleism) VS. 序列(Serial)
從下面的程式碼我們先來看一下序列

dispatch_queue_t queue = dispatch_queue_create("Sereal_Queue_1", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10 ; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"%d",i);
        });
    }
複製程式碼
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 0
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 1
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 2
2017-07-02 01:59:03.214 GCD_Test[86301:1702080] 3
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 4
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 5
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 6
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 7
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 8
2017-07-02 01:59:03.215 GCD_Test[86301:1702080] 9
複製程式碼

從列印結果我們可以清晰的看到,所有被新增到序列佇列的任務都是按照新增順序依次執行的,也就是說序列的基本特點是任務按照順序執行。

並行

dispatch_queue_t concurrentQueue = dispatch_queue_create("Global_Queue_1", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0 ; i < 10 ; i ++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"%d",i);
        });
    }

複製程式碼
2017-07-02 02:02:29.759 GCD_Test[86327:1705240] 2
2017-07-02 02:02:29.759 GCD_Test[86327:1705243] 1
2017-07-02 02:02:29.759 GCD_Test[86327:1705239] 0
2017-07-02 02:02:29.759 GCD_Test[86327:1705268] 5
2017-07-02 02:02:29.759 GCD_Test[86327:1705264] 3
2017-07-02 02:02:29.759 GCD_Test[86327:1705267] 4
2017-07-02 02:02:29.759 GCD_Test[86327:1705270] 7
2017-07-02 02:02:29.759 GCD_Test[86327:1705269] 6
2017-07-02 02:02:29.759 GCD_Test[86327:1705271] 8
2017-07-02 02:02:29.759 GCD_Test[86327:1705273] 9
複製程式碼

列印結果表明任務的新增順序和執行順序無關,並且在使用了 [NSThread sleepForTimeInterval:1.0];的情況下,所有人物的執行時間是一樣的,這說明它們是並行執行的,如果你有興趣的話還可以列印一下它們執行任務的執行緒,這樣將會獲得更清楚的顯示。

GCD出場???

隨著一陣啪啪啪(鍵盤的聲音)?GCD 出場了。
1,為什麼使用GCD?
可以這樣說,GCD為我們提供了一個更加容易的方式來實現多執行緒程式設計,我們不用直接去建立,管理執行緒,而只需要通過GCD幫助我們把任務放入相應的佇列中來實現多執行緒程式設計的特點。
2,為什麼使用多執行緒程式設計?
就我現在不多的經驗來說,(1),多執行緒程式設計使我們在程式設計的過程中將一下耗時的操作放在非主執行緒當中,避免了阻塞主執行緒。(2),在與網路的互動當中提高了效率,比如說我們可以使用多執行緒並行上傳和下載來提高速度。
3,多執行緒程式設計需要注意什麼?
(1),避免死鎖,後面我們會具體說到。
(2),資料競爭,後面我們會具體說到。
(3),避免建立大量的無用執行緒,執行緒的建立和維護是需要消耗系統資源的,執行緒的堆疊都需要建立和維護,因此建立大量執行緒是百害而無一利。因此我們要記住,多執行緒技術本身是沒有好處的,關鍵是要使用多執行緒完成並行操作以此來提高效率。

iOS多執行緒開發—GCD (一)
1.建立佇列

在GCD中有三種不同的佇列:

  • 主佇列:這是一個特殊的序列佇列,佇列中的任務都在主執行緒中執行。
  • 序列佇列:任務在佇列中先進先出,每次執行一個任務。
  • 併發佇列:任務在佇列中也是先進先出,但是同時可以執行多個任務。
//獲取主佇列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//獲取序列佇列
 dispatch_queue_t serialQueue = dispatch_queue_create("com.jiaxiang.serialQueue", DISPATCH_QUEUE_SERIAL);
 
//獲取並行佇列
dispatch_queue_t concurrentQueue1 = dispatch_queue_create("com.jiaxiang.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t concurrentQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
複製程式碼

當我們使用dispatch_queue_create的時候,我們建立了一個序列佇列或者並行佇列,當我們使用dispatch_get_main_queue();時我們獲取了系統建立的主佇列,dispatch_get_global_queue讓我們獲取了系統中的全域性佇列,並且通過引數為全域性佇列設定了優先順序。

全域性佇列有四種優先順序分別用巨集定義

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
複製程式碼

在這裡要說兩個問題:
1,序列佇列,並行佇列,同步,非同步與執行緒之間的關係。
2,死鎖問題是如何產生的。

佇列 同步 非同步
主佇列 在主執行緒執行 在主執行緒執行
序列佇列 在當前執行緒執行 在新建執行緒執行
並行佇列 在當前執行緒執行 在新建執行緒執行

讓我們看以下程式碼來驗證上述結論:

  dispatch_queue_t mainQueue = dispatch_get_main_queue();
 
 //在主執行緒同步追加任務造成死鎖
  dispatch_sync(mainQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });
    
  dispatch_async(mainQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });
複製程式碼
iOS多執行緒開發—GCD (一)
2017-07-02 14:06:40.371 GCD_Test[86684:1876563] <NSThread: 0x600000079000>{number = 1, name = main}
複製程式碼

第一個主佇列同步追加任務會造成死鎖,我們從棧呼叫可以看出以上程式碼是在主執行緒執行。第二主佇列非同步追加任務可以順利執行,我們從列印可以看出是在主執行緒執行。

dispatch_queue_t serialQueue = dispatch_queue_create("com.jiaxiang.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
});
    
dispatch_async(serialQueue, ^{
    NSLog(@"%@",[NSThread currentThread]);
});
複製程式碼
2017-07-02 14:16:58.578 GCD_Test[86721:1886346] <NSThread: 0x60000006f1c0>{number = 1, name = main}
2017-07-02 14:16:58.579 GCD_Test[86721:1886393] <NSThread: 0x608000074c00>{number = 3, name = (null)}
複製程式碼

第一個是在當前執行緒,因為當前執行緒是主執行緒,所以也就是在主執行緒執行。第二個是新建執行緒執行。

dispatch_queue_t concurrentQueue1 = dispatch_queue_create("com.jiaxiang.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue1, ^{
    NSLog(@"%@",[NSThread currentThread]);
  });
    
dispatch_async(concurrentQueue1, ^{
      NSLog(@"%@",[NSThread currentThread]);
  });
複製程式碼
2017-07-02 14:20:26.646 GCD_Test[86748:1890613] <NSThread: 0x60800007d300>{number = 1, name = main}
2017-07-02 14:20:26.646 GCD_Test[86748:1890662] <NSThread: 0x6080002675c0>{number = 3, name = (null)}
複製程式碼

第一個是在當前執行緒,因為當前執行緒是主執行緒,所以也就是在主執行緒執行。第二個是新建執行緒執行。

通過以上的程式碼我們可以看出,並行佇列不一定會新建執行緒,序列佇列也不一定只在當前執行緒執行。因此,當我們考慮佇列是在哪個執行緒執行的時候我們一定要考慮它是同步還是非同步執行的問題

下面我們來看死鎖:
其實對於死鎖我覺得大家記住這句話就夠了,在當前序列佇列中同步追加任務必然造成死鎖。
比如我們上面用到的在主佇列中同步新增任務,因為dispatch_sync會阻塞當佇列程直到block中追加的任務執行完成之後在繼續執行,但是block中的任務是被新增到主佇列最後的位置,那麼主佇列中其他任務如果不完成的話追加的block是不會執行的,但是佇列被阻塞,block前面的任務無法執行,這就造成了在主佇列中任務互相等待的情況,最終造成死鎖。
在分析死鎖問題是不要過多的考慮使用的是什麼執行緒,因為我們在使用GCD的時候首先考慮的是佇列和任務,至於執行緒的分配和維護是由系統決定的,如果我們總是考慮執行緒那樣往往會使我們難以分析死鎖的原因。
至於解決死鎖的方法其實也很簡單,使用dispatch_async避免當前佇列被阻塞,這樣我們就可以在不等待追加到佇列最後的任務完成之前繼續執行佇列中的任務。並將追加的任務新增到佇列末尾。

參考文章:
《iOS開發之多執行緒程式設計總結(一)》
《Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理》
《Effective Objective-C 2.0》

相關文章