GCD
全稱Grand Central Dispatch
是蘋果提供的一個多核編程的解決方案,在真正意義上實現了并行
操作,而不是并發。
GCD
使用線程池
模型來執行用戶提交的任務,所以它比較節約資源,不需要為每個任務都重新創建一個新的線程,GCD
不需要自行編寫并行代碼,而是自動進行多核的并行計算,自動管理線程的生命周期,如:使用線程池管理線程的創建和銷毀,線程的調度,任務的調度等,用戶只需要編寫任務代碼并提交即可。
GCD
中有兩個比較重要的概念:任務
和隊列
。
GCD的任務:
任務顧名思義就是我們需要執行的代碼塊,可以是一個方法也可以是一個block
,就是我們需要線程為我們完成的工作,編寫完成的任務只需提交給GCD
的隊列,即可自動幫我們完成任務的調度,以及線程的調度,可以很方便的以多線程的方式執行。
GCD的隊列:
隊列用于管理用戶提交的任務,GCD的隊列有兩種形式,串行隊列和并發隊列:
串行隊列: GCD底層只維護一個線程,任務只能串行依次執行。
對于串行隊列
來說,GCD
每次從串行隊列
的隊首取一個任務交給唯一的一個線程來處理,直到前一個任務完成后,才繼續從隊列中取下一個任務來執行,因此,串行隊列
中的任務執行嚴格按照提交順序
,并且后一個任務必須等前一個任務執行完成后才可以執行。并發隊列: GCD底層使用線程池維護多個線程,任務可并發執行。
對于并發隊列
來說,GCD
每次從并發隊列的隊首取一個任務,并將這個任務按照任務調度分發給多個線程中的某一個線程,此時不需要等待其完成,如果隊列中還有其他任務繼續從隊列中取出并分發給某一個線程來執行,由于底層由線程池
管理多個線程,每個任務的時間復雜度不同再加上線程調度的影響,后提交的任務可能先執行完成。但對于單個線程來說,只能按順序執行,比如某個線程被安排了多個任務,那這個線程就只能按提交順序依次執行任務。
不論是串行隊列還是并發隊列都使用FIFO 先進先出
的方式來管理用戶提交的任務。
所以,我們在使用GCD時也就很簡單了,只需要創建或獲取系統隊列、編寫任務并提交任務到隊列即可。首先舉一個下載圖片的栗子,這個栗子和第一篇講解NSThread的栗子一樣,但是使用GCD來實現:
//獲取一個優先級默認的全局并發隊列,并以異步的方式提交任務執行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下載圖片
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
//獲取主隊列,在主線程中更新UI,并以異步方式提交任務
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
上面的栗子非常簡單,不需要我們手動創建線程即可實現多線程的并發編程,如果現在還看不懂沒關系,學完本章內容你一定會懂。
GCD創建隊列和獲取隊列的API:
//獲取當前執行該方法的隊列,被廢棄了,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);
/*
獲取主隊列,即與主線程相關聯的隊列
如果需要提交任務到主線程使用該方法獲取主線程的主隊列即可
主隊列是串行隊列因為只維護主線程一個線程
*/
dispatch_queue_t dispatch_get_main_queue(void);
/*
獲取一個全局的并發隊列
identifier指定該隊列的優先級可選值有:
DISPATCH_QUEUE_PRIORITY_HIGH 2
DISPATCH_QUEUE_PRIORITY_DEFAULT 0
DISPATCH_QUEUE_PRIORITY_LOW (-2)
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到傳0
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
/*
創建一個隊列
label 隊列的名稱
attr 隊列的屬性可選值有:
DISPATCH_QUEUE_SERIAL 創建一個串行隊列
DISPATCH_QUEUE_CONCURRENT 創建一個并發隊列
通過這種方式可以自己維護一個隊列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
GCD的執行方式:
- 同步執行:阻塞當前線程,直到任務執行完成后,當前線程才可繼續執行。
- 異步執行:不阻塞當前線程,可能使用其他線程來執行任務,不需要等待任務完成,當前線程即可立即繼續執行。
API:
/*
以異步方式執行任務,不阻塞當前線程
queue 管理任務的隊列,任務最終交由該隊列來執行
block block形式的任務,該block返回值、形參都為void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
/*
同上
context 是一個void*的指針,作為work的第一個形參
work 是一個函數指針,指向返回值為void 形參為void*的函數,且形參不能為NULL,也就是說context一定要傳
使用起來不方便,一般不怎么用,需要使用C函數,也可以使用OC方法通過傳遞IMP來執行但是會有編譯警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式執行任務,阻塞當前線程,必須等待任務完成當前線程才可繼續執行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任務,并重復執行iterations次
iterations 迭代執行次數
queue 管理任務的隊列,任務最終交由該隊列來執行
block block形式的任務,該block返回值為void形參為iterations迭代次數
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue, DISPATCH_NOESCAPE void (^block)(size_t));
//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));
/*
以異步方式提交任務,在when時間點提交任務
queue 管理任務的隊列,任務最終交由該隊列來執行
block block形式的任務,該block返回值、形參都為void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以異步方式提交任務,會阻塞queue隊列,但不阻塞當前線程
queue 管理任務的隊列,任務最終交由該隊列來執行
需要說明的是,即時使用并發隊列,該隊列也會被阻塞,前一個任務執行完成才能執行下一個任務
block block形式的任務,該block返回值、形參都為void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任務,會阻塞queue隊列,也會阻塞當前線程
queue 管理任務的隊列,任務最終交由該隊列來執行
同樣的,即時是并發隊列該隊列也會被阻塞,需要等待前一個任務完成,同時線程也會阻塞
block block形式的任務,該block返回值、形參都為void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
底層線程池控制block任務在整個應用的生命周期內只執行一次
predicate 實際為long類型,用于判斷是否執行過
block block形式的任務,該block返回值、形參都為void
該方法常用于實現單例類,以及結合RunLoop創建一個常駐內存的線程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
猛的一看常用的方法就有十二種呢,但是可以發現每一類方法都提供了block任務
和function任務
兩種形式,所以常用的也就七種,只是對應了block版本和函數版本。接下來先介紹最常用的同步執行和異步執行,其他的方法后文會講。
GCD隊列和執行方式的組合:
-
異步并發隊列:
//手動創建了一個并發隊列 dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); //也可以獲取全局的并發隊列,效果一樣 //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //開啟異步任務,并加入并發隊列中 dispatch_async(concurrentQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印結果:
2020-03-17 11:21:14.225080+0800 多線程[2918:102124] Task2 <NSThread: 0x600003a25400>{number = 9, name = (null)} 2020-03-17 11:21:14.225108+0800 多線程[2918:103405] Task1 <NSThread: 0x600003a254c0>{number = 11, name = (null)} 2020-03-17 11:21:14.225367+0800 多線程[2918:103431] Task4 <NSThread: 0x600003a20f80>{number = 13, name = (null)} 2020-03-17 11:21:14.225491+0800 多線程[2918:103407] Task3 <NSThread: 0x600003a25500>{number = 12, name = (null)}
可以看出開辟多個線程,每個任務都是在不同的線程中同時執行。
-
異步串行隊列:
//手動創建了一個串行隊列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); //開啟異步任務,并加入串行隊列中 dispatch_async(serialQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印結果:
2020-03-17 11:27:59.970147+0800 多線程[3004:107449] Task1 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970364+0800 多線程[3004:107449] Task2 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970544+0800 多線程[3004:107449] Task3 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970682+0800 多線程[3004:107449] Task4 <NSThread: 0x600002394780>{number = 7, name = (null)}
可以看出開辟出一個新的線程,并且所有任務都在這個線程中按照順序執行。
-
同步并發隊列:
//手動創建了一個并發隊列 dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); //也可以獲取全局的并發隊列,效果一樣 //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //開啟同步任務,并加入并發隊列中 dispatch_sync(concurrentQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印結果:
2020-03-17 11:32:51.963617+0800 多線程[3081:111266] Task1 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.963813+0800 多線程[3081:111266] Task2 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.963920+0800 多線程[3081:111266] Task3 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.964038+0800 多線程[3081:111266] Task4 <NSThread: 0x600001204c00>{number = 1, name = main}
可以看出沒有開辟新的線程,每個任務都是在當前線程中按照順序執行。
-
同步串行隊列:
//手動創建了一個串行隊列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); //開啟同步任務,并加入串行隊列中 dispatch_sync(serialQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印結果:
2020-03-17 11:39:27.880465+0800 多線程[3162:115800] Task1 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880655+0800 多線程[3162:115800] Task2 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880791+0800 多線程[3162:115800] Task3 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880908+0800 多線程[3162:115800] Task4 <NSThread: 0x6000020b25c0>{number = 1, name = main}
可以看出沒有開辟新的線程,每個任務都是在當前線程中按照順序執行。
GCD隊列和執行方式的組合總結:
type | Serial串行隊列 | Concurrent并發隊列 |
---|---|---|
async異步執行 | 不阻塞當前線程,使用其他線程串行執行任務,只有一個線程按順序執行任務 | 不阻塞當前線程,并發執行任務,使用多個線程同時執行任務 |
sync同步執行 | 阻塞當前線程,使用同一線程串行執行任務,只有一個線程用于執行任務 | 阻塞當前線程,可能使用同一線程串行執行任務 |
所以,針對異步執行/同步執行和串行隊列/并發隊列,只需要掌握其關鍵就可以了,同步/異步的區別在于是否阻塞線程,串行/并發隊列的區別在于有多少個線程參與執行任務。即時存在嵌套結構也能夠很好理解了。舉一個嵌套結構的例子:
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
for (int i = 0; i < 100; i++)
{
NSLog(@"Complete.");
}
});
外層dispatch不論使用串行隊列還是并發隊列,由于只有一個任務,只會有一個線程來執行這個block塊的內容,而同步和異步的區別就在于是否會阻塞當前線程,接下來看block塊的內容,采用了三個異步提交到并發隊列,所以并發隊列里就有了三個不同的任務,就可以真正執行并發,由于都是異步提交沒有阻塞當前線程,所以輸出Complete
的代碼也會摻雜在Task1-3
中亂序輸出。
GCD其他方法的使用:
-
dispatch_apply:表示重復執行任務
iterations
次。同步執行
,會阻塞當前線程。dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) { NSLog(@"Task %@ %ld", [NSThread currentThread], t); });
注意:如果該方法是在主線程中執行,則不能傳入當前串行隊列。因為
dispatch_apply
方法是是同步執行,又把該方法放入當前串行隊列中去執行,但是當前串行隊列處于阻塞中,等待dispatch_apply
方法執行完成,所以會產生死鎖。 -
dispatch_after:等待
when
時間后,執行任務。異步執行
,不會造成線程阻塞。NSLog(@"Before"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"In %@", [NSThread currentThread]); }); NSLog(@"After");
打印結果:
2020-03-17 14:58:27.403048+0800 多線程[4687:212802] Before 2020-03-17 14:58:27.403355+0800 多線程[4687:212802] After 2020-03-17 14:58:32.403767+0800 多線程[4687:212986] In <NSThread: 0x6000007a6040>{number = 7, name = (null)}
可以看出這個5s并不是精確的5s,因為該方法是在when時間點到達的時候去提交任務到隊列,所以是延遲提交,而不是延遲執行,隊列什么時候安排線程去執行是未知的,所以不要用這個方法去實現定時器這樣的功能。
-
dispatch_barrier_(a)sync:柵欄函數,用于阻塞當前隊列,等待當前隊列任務執行完成后,在執行
dispatch_barrier_(a)sync
的任務。dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task0 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); } }); dispatch_barrier_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task2 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task3 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task4 %@ %d", [NSThread currentThread], i); } });
上面的輸出是按照
Task0
Task1
并發執行,Task2
等待Task0
Task1
執行完成后單獨執行, 最后Task3
Task4
等待Task2
執行完成后開始并發執行 。dispatch_barrier_async
方法常與并發隊列
共用,前一段任務使用dispatch_async
異步并發執行,然后插入一個dispatch_barrier_async
執行一個中間任務,這個中間任務必須要等待前面的并發任務執行完成后才能開始執行,接著這個中間任務完成后,繼續異步并發執行接下來的任務。 -
dispatch_once:該方法能夠保證在應用的生命周期內只執行一次提交的任務,所以常用于單例類的創建。
@interface MyUtil: NSObject <NSCopying> + (instancetype)sharedUtil; @end @implementation MyUtil static MyUtil *staticMyUtil = nil; + (instancetype)sharedUtil { //保證初始化創建只執行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticMyUtil = [[MyUtil alloc] init]; }); return staticMyUtil; } //防止通過alloc或new直接創建對象 + (instancetype)allocWithZone:(struct _NSZone *)zone { //保證alloc函數只執行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticMyUtil = [super allocWithZone:zone]; }); return staticMyUtil; } //實現NSCopying協議的方法,防止通過copy獲取副本對象 - (instancetype)copyWithZone:(NSZone *)zone { return staticMyUtil; } @end
dispatch_once
函數需要傳入一個long類型的predicate
,這個值必須是獨一無二的,使用靜態變量的地址最合適不過了,MyUtil
實現了NSCopying
協議的copyWithZone:
方法,防止通過copy
方法獲取副本對象。當使用
alloc&&init
方法初始化時,先調用allocWithZone:
方法來分配存儲空間,如果再次使用sharedUtil
方法來獲取的話,由于沒有執行過,會執行到dispatch_once
內部block
,此時會再去執行allocWithZone:
方法,但該方法內部dispatch_once
已經執行過了會直接返回staticMyUtil
,反過來調用是一樣的道理,通過這樣的方式就可以實現真正的單例了。 -
dispatch_ group_ t:是一個比較實用的方法,通過構造一個組的形式,將各個同步或異步提交任務都加入到同一個組中,當所有任務都完成后會收到通知,用于進一步處理,通過這樣的方式就可以實現多線程下載,當下載完成后就可以通知用戶了。
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); } }); dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task2 %@ %d", [NSThread currentThread], i); } }); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task3 %@ %d", [NSThread currentThread], i); } }); dispatch_group_notify(group, concurrentQueue, ^{ NSLog(@"All Task Complete"); });
像一個組中添加了三個異步的任務,最終三個任務完成后可以收到通知執行回調的
block
,上面的輸出為,All Task Complete
在前面三個輸出都結束后才會輸出。
防止GCD產生死鎖:
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"Before");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"In");
});
NSLog(@"After");
}
上述代碼就會產生死鎖,分析下原因,首先,viewWillAppear:
方法是在主線程
中執行的,接著調用dispatch_sync
方法,該方法會阻塞
當前線程,也就是會阻塞主線程
,主線程被阻塞是為了等待任務的完成,然后該代碼將任務添加
到了主隊列
,主隊列會將任務交給主線程執行,但此時主線程阻塞
了,任務添加進了主線程得不到運行,而主線程在等待任務的執行
,因此就造成了死鎖
。
這個栗子一般人寫不出來這樣的代碼,僅僅是為了講解什么情況下會造成死鎖,即,線程被阻塞需要等到任務執行完成,而任務由于線程阻塞得不到執行。前文舉的幾個串行隊列的栗子很多是不能使用主隊列的,原因也正在此。
dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
NSLog(@"===== %@ %ld", [NSThread currentThread], t);
});
});
上述栗子也會造成死鎖,因為,dispatch_apply
同樣會阻塞
當前線程,它需要等待內部的dispatch_apply
執行完成,內部的需要等待外部的線程來執行它,產生了死鎖。
結論:使用sync同步函數
往當前串行隊列
中添加任務,會卡主當前的串行隊列(產生死鎖)。