前言
明天要給師弟開分享會,分享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, ^{
// 這裡放非同步執行任務程式碼
});
複製程式碼
個人認為易迷惑的點
- 太多的組合方式 有兩種任務執行方式,兩種佇列+特殊的主佇列,就可以組成六種組合。 有兩張圖總結得特別好,記住這兩張圖,分析的時候用得到。 然後為了更好理解,自己也花了點時間弄了動圖。
還是不能忘了《Inside》的例子。
-
兩種待辦任務表(對應佇列) 一種是多個機器人對多個寶藏,先入先出發。(對應並行佇列) 另一種是一個機器人對有順序找的多個寶藏。(對應序列佇列) 特殊的 強行自己去做的任務表。 (對應主佇列)
-
你有兩類事情(對應任務) 一類是吃喝拉撒,一有需要就自己馬上去做,總不能懶到讓機器人幫忙吧。(對應著同步執行任務) 另一類是尋寶,要機器人去做,出發前要點時間給機器人充電。(對應著非同步執行任務)
程式碼中, 輸出@"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---");
}
複製程式碼
多個機器人找多個寶藏,完成程度和你的吃喝拉撒沒必然先後順序。
- 非同步 + 序列佇列 (一個機器人找有序寶藏)
- (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---");
}
複製程式碼
一個機器人等有序寶藏圖拼接好後,就出發了。和你吃喝拉撒沒先後順序。
- 同步 + 並行佇列 (自己吃喝拉撒 放到 多個機器人對多個寶藏,準備好後機器人一起出發)
- (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”。有著必然先後順序。
- 同步+ 序列佇列 (自己吃喝拉撒 放到 一個機器人對有順序找的多個寶藏)
- (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”。有著必然先後順序。
- 非同步 + 主佇列 (讓機器人充電準備尋寶 放到 強行自身去做的任務表 )
- (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"。 然後你發現這件事是在強制自己做的任務表上,於是就自己一件接一件做了。- 同步+主佇列(死鎖)(吃喝拉撒 + 強行自身去做)
- (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]);
});
}
複製程式碼
可以這麼理解
- 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作為鎖物件,這樣可以避免單獨再建立一個鎖物件。
方法二:自旋鎖
用到屬性修飾原子屬性nonatomic
和 atomic非原子屬性
- atomic:保證同一時間只有一個執行緒能夠寫入,讀取隨意
- nonatomic:同一時間可以有很多執行緒讀和寫 atomic帶有自旋鎖,別的執行緒如果寫入,就會開啟runloop。 但是迴圈執行的執行緒,會消耗不少資源。所以一般開發中,除非確定不然不要用atomic。
參考
力薦第三篇,看了很多瞎說的,就這篇真實!!!