iOS GCD入門和GCD對CPU多核的使用

Hsusue發表於2018-08-02

前言

好開心

明天要給師弟開分享會,分享GCD。 好方,只理解一些皮毛拿什麼去裝。準備的時候順便把過程記錄下來。

目錄

  • 概念
  • 簡單瞭解用法
  • 開發中常用的做法
  • GCD其他的一些API
  • GCD會遇到的問題

和GCD有關的基本概念

術語 含義
程式 開啟一個App就是開啟一個程式。
執行緒 獨立執行的程式碼段,一個執行緒同時間只能執行一個任務,反之多執行緒併發就可以在同一時間執行多個任務。在iOS系統中,一個程式包含一個主執行緒,它的主要任務是處理UI。其他執行緒稱為子執行緒。
同步 A執行完再執行B。
非同步 A和B可以同時執行。
任務 可以理解為某一堆要執行的程式碼。分為同步執行任務和非同步執行任務。用block定義。
同步執行任務 按進入順序執行的任務
非同步執行任務 不管進入順序,可以一起執行
佇列 存放任務的結構。分為序列佇列和並行佇列。遵循先進先出。
佇列組 將多執行緒進行分組,最大的好處是可獲知所有執行緒的完成情況。
序列佇列 執行緒執行只能依次逐一先後有序的執行。
並行佇列 指兩個或多個事件在同一時刻發生。多核CUP同時開啟多條執行緒供多個任務同時執行,互不干擾。
併發 指兩個或多個事件在同一時間間隔內發生。可以在某條執行緒和其他執行緒之間反覆多次進行上下文切換,看上去就好像一個CPU能夠並且執行多個執行緒一樣。其實是偽非同步。
  • 一個有助於判斷執行完成時間的理論。 開執行緒需要消耗記憶體,所以要消耗時間

回頭看覺得有必要在這簡單說明多執行緒 多核 併發並行的區別子執行緒和主執行緒的聯絡

最近玩了個遊戲叫《Inside》,戴著頭盔就能操縱機器人,感覺無論是玩法還是遊戲劇情都超適合類比執行緒。 用這個舉個例子。 假如你是國王,拿到了一張藏寶圖,但這個寶藏要到每一個地點才能得知下一個地點的資訊(電路中記憶體地址)。於是你就操縱機器人A去找,找到後帶回來。機器人A的路線就是一條執行緒。 當機器人A還在路程上,你又得到一張藏寶圖。你這時候派機器人B去找,找到帶回來。這時候機器人B的路線就是另一條執行緒。 以上就是多執行緒。 這時候,只要你週期足夠短,輪流戴頭盔a和頭盔b,,看上去就像你同時在操縱機器人A和機器人B。這就叫做併發!裝出來的。 某一天,你的頭快搖傻了。於是乎你長出了第二個頭。(對應著雙核CPU),這時候就是名副其實地同時操縱。這就叫並行,必須要多頭怪才擁有這技能。 但如果又操縱第三個機器人,這時候只能再來回戴了,又要併發了。 A找到並回到了城堡把結果帶回給你,你才發現你也是個機器人(主執行緒)。其他機器人帶回寶藏後就可以拜拜了,但就算還有沒有寶藏在路上,你都不能拜拜,必須保持呼吸(runloop)。 這就是子執行緒和主執行緒的聯絡。 子執行緒的任務全部完成後,最終會回到主執行緒。主執行緒中執行著runloop

簡單瞭解用法

就是把任務加到佇列中 佇列可以自己新建。 系統也有 全域性併發佇列主佇列

#pragma mark - 建立佇列
//  建立佇列
//  第一個引數 佇列名稱
//  第二個引數的作用:序列(DISPATCH_QUEUE_SERIAL)、並行(DISPATCH_QUEUE_CONCURRENT)。
  dispatch_queue_t queue = dispatch_queue_create("net.Hsusue.testQueue", DISPATCH_QUEUE_CONCURRENT);
* 常用的系統併發佇列——全域性併發佇列
//程式預設的佇列級別,一般不要修改,DISPATCH_QUEUE_PRIORITY_DEFAULT == 0
dispatch_queue_t globalQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//HIGH
dispatch_queue_t globalQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//LOW
dispatch_queue_t globalQueue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//BACKGROUND
dispatch_queue_t globalQueue4 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
// 獲取主佇列有特別函式(是個序列佇列)
//  dispatch_queue_t queue = dispatch_get_main_queue();
#pragma mark - 建立任務加到佇列中
//  同步執行任務建立方法
  dispatch_sync(queue, ^{
  // 這裡放同步執行任務程式碼
  });
// 非同步執行任務建立方法
  dispatch_async(queue, ^{
  // 這裡放非同步執行任務程式碼
  });
複製程式碼

個人認為易迷惑的點

  • 太多的組合方式 有兩種任務執行方式,兩種佇列+特殊的主佇列,就可以組成六種組合。 有兩張圖總結得特別好,記住這兩張圖,分析的時候用得到。 然後為了更好理解,自己也花了點時間弄了動圖。

image.png

image.png

還是不能忘了《Inside》的例子。

  • 兩種待辦任務表(對應佇列) 一種是多個機器人對多個寶藏,先入先出發。(對應並行佇列) 另一種是一個機器人對有順序找的多個寶藏。(對應序列佇列) 特殊的 強行自己去做的任務表。 (對應主佇列)

  • 你有兩類事情(對應任務) 一類是吃喝拉撒,一有需要就自己馬上去做,總不能懶到讓機器人幫忙吧。(對應著同步執行任務) 另一類是尋寶,要機器人去做,出發前要點時間給機器人充電。(對應著非同步執行任務)

程式碼中, 輸出@"1"對應著吃喝拉撒

  1. 非同步 + 並行佇列 (多個機器人找多個寶藏)
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self asyncConcurrent];
    NSLog(@"1");
}

//非同步執行 + 並行佇列
- (void)asyncConcurrent{
    //建立一個並行佇列
    dispatch_queue_t queue = dispatch_queue_create("識別符號", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"---start---");
    
    //使用非同步函式封裝三個任務
    dispatch_async(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    
    NSLog(@"---end---");
}
複製程式碼

多個機器人找多個寶藏

非同步 + 並行佇列(程式碼一開始是任務123)
多個機器人找多個寶藏,完成程度和你的吃喝拉撒沒必然先後順序。

  1. 非同步 + 序列佇列 (一個機器人找有序寶藏)
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
    [self asyncSerial];
    NSLog(@"1---%@", [NSThread currentThread]);
}
//非同步 + 序列佇列
- (void)asyncSerial{
    //建立一個序列佇列
    dispatch_queue_t queue = dispatch_queue_create("識別符號", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"---start---");
    //使用非同步函式封裝三個任務
    dispatch_async(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    NSLog(@"---end---");
}
複製程式碼

一個機器人找有序寶藏

非同步 + 序列佇列
一個機器人等有序寶藏圖拼接好後,就出發了。和你吃喝拉撒沒先後順序。

  1. 同步 + 並行佇列 (自己吃喝拉撒 放到 多個機器人對多個寶藏,準備好後機器人一起出發)
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
    [self syncConcurrent];
    NSLog(@"1---%@", [NSThread currentThread]);
}
//同步 + 並行佇列
- (void)syncConcurrent{
    //建立一個並行佇列
    dispatch_queue_t queue = dispatch_queue_create("識別符號", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"---start---");
    //使用同步函式封裝三個任務
    dispatch_sync(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    NSLog(@"---end---");
}
複製程式碼

自己吃喝拉撒 放到 多個機器人對多個寶藏,準備好後機器人一起出發

同步 + 並行佇列
一要吃喝拉撒就自己馬上去做。所以不等@“end”輸出就先做完了。最後再@“1”。有著必然先後順序。

  1. 同步+ 序列佇列 (自己吃喝拉撒 放到 一個機器人對有順序找的多個寶藏)
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
//    [self syncConcurrent]
    [self syncSerial];
    NSLog(@"1---%@", [NSThread currentThread]);
}

//同步 + 序列佇列
- (void)syncSerial{
    //建立一個序列佇列
    dispatch_queue_t queue = dispatch_queue_create("識別符號", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"---start---");
    //使用非同步函式封裝三個任務
    dispatch_sync(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    NSLog(@"---end---");
}
複製程式碼

同步 + 序列佇列
和上面一樣 偷懶沒改字
這次更過分了,試圖讓一個機器人幫自己拉三次尿。。。但機器人做不到。

一要吃喝拉撒就自己馬上去做。所以不等@“end”輸出就先做完了。最後再@“1”。有著必然先後順序。

  1. 非同步 + 主佇列 (讓機器人充電準備尋寶 放到 強行自身去做的任務表 )
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
//    [self syncConcurrent]
//    [self syncSerial];
    [self asyncMain];
    NSLog(@"1---%@", [NSThread currentThread]);
}

//非同步 + 主佇列
- (void)asyncMain{
    //獲取主佇列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    NSLog(@"---start---");
    //使用非同步函式封裝三個任務
    dispatch_async(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    NSLog(@"---end---");
}
複製程式碼

和非同步 + 序列佇列區別就是不開啟新執行緒。

非同步 + 主佇列
讓機器人充電準備,所以自己先吃喝拉撒完。直到@"1"。 然後你發現這件事是在強制自己做的任務表上,於是就自己一件接一件做了。

  1. 同步+主佇列(死鎖)(吃喝拉撒 + 強行自身去做)
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
//    [self syncConcurrent]
//    [self syncSerial];
//    [self asyncMain];
    [self syncMain];
    NSLog(@"1---%@", [NSThread currentThread]);
}
//同步+主佇列(死鎖)
- (void)syncMain{
    //獲取主佇列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    NSLog(@"---start---");
    //使用同步函式封裝三個任務
    dispatch_sync(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務B---%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務C---%@", [NSThread currentThread]);
    });
    NSLog(@"---end---");
}
複製程式碼

同步+主佇列(死鎖)
這裡認真分析一下死鎖的原因,不用那個例子了。 先說點計算機組成原理的知識,雖然我也學得很爛。 計算機指令包括操作碼和地址碼。 每個函式進入都會記住進入的地址碼,return時就會回去。

上面主佇列在主佇列中加了任務。 實質在同一個同步序列佇列中,再使用該序列佇列同步的執行任務。

[self syncMain]這是主佇列做(出)的事(同步且未做完)。根據先進先出,主佇列頭是syncMain。然後假設這裡的記憶體地址是1。

dispatch_sync(queue, ^{
        NSLog(@"任務A---%@", [NSThread currentThread]);
    });
// 假設執行時此處記憶體地址為1
複製程式碼

新增了一個block到主佇列尾部,要等主佇列頭synMain執行完才能執行。 本來應該執行追加任務B,但是電路上的地址並沒有回來,因為dispatch_sync要執行完block才reutrn。 因為被程式碼被黑盒子包起來了,大膽猜測一下。 假設記憶體地址為2

// 呼叫時記住進入地址為1
dispatch_sync {
  // block執行完才return
  // 執行時此處記憶體地址為2
   if( block() ) { // block執行完
     return;//返回到進入地址
   }
}
複製程式碼

於是程式碼可以看成 卡在了該函式內部,記憶體地址為2處。 沒有回到1處,自然就不會追加任務B。

開發中常用的做法

上面說了很多種方法,禁止死鎖情況開發中是很容易記住的。 但其他組合,即使想的時候能想懂,但也還是很混亂。 根據我個人經驗,日常開發中先從巨集觀上想是否需要耗時(耗時放到子執行緒),是否有序。 通常是需要和主執行緒同時執行(開新執行緒,即非同步執行任務)才會用到GCD。 可能是開發經驗不夠。

  • 非同步 + 並行或序列。 舉個例子。

從子執行緒,非同步返回主執行緒更新UI。 佇列常用全域性並行佇列。 因為要下載圖片耗時,而且具有網路不穩定性,所以放到子執行緒。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3948453733,2367168123&fm=27&gp=0.jpg"]];
        UIImage *image = [UIImage imageWithData:imgData];
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        dispatch_async(mainQueue, ^{
            UIImageView *imgView = [[UIImageView alloc]initWithImage:image];
            [imgView setFrame:CGRectMake(0, 0, 200, 200)];
            [imgView setCenter:self.view.center];
            [self.view addSubview:imgView];
        });
    });

複製程式碼
  • 佇列組 佇列組能獲知佇列完成程度。 同時下載多個圖片,所有圖片下載完成之後去更新UI。
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
//    [self syncConcurrent]
//    [self syncSerial];
//    [self asyncMain];
//    [self syncMain];
    [self groupTest];
    NSLog(@"1---%@", [NSThread currentThread]);
}

- (void)groupTest {
    dispatch_queue_t conCurrentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_group_t groupQueue = dispatch_group_create();
    NSLog(@"current task");
    dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{
        NSLog(@"並行任務1");
    });
    dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{
        NSLog(@"並行任務2");
    });
    dispatch_group_notify(groupQueue, mainQueue, ^{
        NSLog(@"groupQueue中的任務 都執行完成,回到主執行緒更新UI");
    });
    NSLog(@"next task");
}
複製程式碼

佇列組測試
1.dispatch_group_t groupQueue = dispatch_group_create();用於生成佇列組 2.生成佇列時加上字首_guoup 3.dispatch_group_notify這個函式用以處理其他佇列完成的塊。

GCD其他的API

  • dispatch_once:這個函式保證在應用程式執行中只執行一次指定處理的API。(見過用於音樂播放器單例)
static dispatch_once_  onceToken;

dispatch_once( &onceToken,^{

物件A =[ [物件A  alloc] init];

});
複製程式碼
  • dispatch_barrier_async:柵欄方法。用於在同一個佇列中,阻斷前後的任務。
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    [self asyncConcurrent];
//    [self asyncSerial];
//    [self syncConcurrent]
//    [self syncSerial];
//    [self asyncMain];
//    [self syncMain];
//    [self groupTest];
    [self barrier];
    NSLog(@"1---%@", [NSThread currentThread]);
}

// 欄柵函式
- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任務A---%@",[NSThread currentThread]);      
    });
    dispatch_async(queue, ^{
        NSLog(@"任務B---%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"欄柵函式---%@",[NSThread currentThread]);
    });
//  換成同步執行也一樣
//  dispatch_barrier_sync(queue, ^{
//        NSLog(@"欄柵函式---%@",[NSThread currentThread]);
//    });
    dispatch_async(queue, ^{
        NSLog(@"任務C---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務D---%@",[NSThread currentThread]);
    });
}
複製程式碼

欄柵函式
可以這麼理解
image.png

  • dispatch_after:延時執行方法,時間並不精準。我常用其他延時方法,不展開談論這個。
  • dispatch_apply:快速迭代方法。 for必須按順序同步遍歷,dispatch_apply可以同時遍歷多個數字。相當於開執行緒遍歷。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_apply(10, queue, ^(size_t i) {
        NSLog(@"%zd----%@", i, [NSThread currentThread];
    }
複製程式碼
  • 還有一些別的不常用就不展開了。

GCD會遇到的問題

  • 死鎖 上面解釋過了
  • 執行緒安全 場景:兩條不同的執行緒之間同時對一個資料I/O。 比如商品數量 count = 10 , 單價price = 2 單件重量 = 0.1 A執行緒要取某個商品數量,算出商品總價,商品總重量。 B執行緒修改商品數量。 假如A先算出商品總價20,這時B突然修改了count = 11,那A算出的重量是1.1,而不是期望的10。 解決方法: 先簡單理解執行緒和runloop。主執行緒必定會開一條runloop。但子執行緒預設是不開啟的。開啟了runloop就會執行某個機制,讓執行緒在迴圈,不至於銷燬。 所以我們可以在A訪問到count時,對count加鎖,別的執行緒只可以取值,不可以寫入。這時別的執行緒如果訪問不到,就會開啟runloop,不定時訪問,看看count解鎖沒有。 加鎖方法 方法一 互斥鎖(同步鎖)
  @synchronized(鎖物件) {
    // 需要鎖定的程式碼
}
複製程式碼

判斷的時候鎖物件要存在,如果程式碼中只有一個地方需要加鎖,大多都使用self作為鎖物件,這樣可以避免單獨再建立一個鎖物件。 方法二:自旋鎖 用到屬性修飾原子屬性nonatomicatomic非原子屬性

  • atomic:保證同一時間只有一個執行緒能夠寫入,讀取隨意
  • nonatomic:同一時間可以有很多執行緒讀和寫 atomic帶有自旋鎖,別的執行緒如果寫入,就會開啟runloop。 但是迴圈執行的執行緒,會消耗不少資源。所以一般開發中,除非確定不然不要用atomic。

參考

力薦第三篇,看了很多瞎說的,就這篇真實!!!

相關文章