本篇文章是iOS多線程系列的第二篇文章,之所以將GCD放在第二篇介紹,是因為理解了GCD后就比較容易理解NSOperation,NSOperation是蘋果對GCD的封裝的產(chǎn)物,以便我們開發(fā)中更好地使用。
本篇文章主要內(nèi)容:
- GCD是什么
- 學(xué)習(xí)GCD之前需要理解的東西
- 常見GCD函數(shù)的用法及實例演示
GCD是什么
GCD(Grand Central Dispatch),是libdispatch的市場名稱,是Apple開發(fā)的一個多線程編程的優(yōu)化方案,Apple也推薦開發(fā)者使用此方案。libdispatch基于線程池的模式管理、執(zhí)行并行任務(wù)。GCD大大降低了開發(fā)者維護線程的成本,使用起來也更加便捷、愉悅。
學(xué)習(xí)GCD之前需要理解的東西
俗話說:磨刀不誤砍柴工,理解多線程的相關(guān)知識對于深入學(xué)習(xí)GCD是十分必要的。作為計算機相關(guān)專業(yè)的學(xué)生,多線程的學(xué)習(xí)是基礎(chǔ)同時也是必不可少,其內(nèi)容豐富,本篇文章只做大致回顧,不再深入探討。注:以下術(shù)語及概念為個人理解表述
串行 VS 并發(fā)
串行和并發(fā),是用來描述任務(wù)與任務(wù)之間的相對關(guān)系。串行指的是當(dāng)前任務(wù)執(zhí)行時其他任務(wù)需要等待,等待當(dāng)前任務(wù)完成后,才能繼續(xù)下一個任務(wù),任務(wù)一個一個地執(zhí)行;并發(fā)指的是當(dāng)前任務(wù)執(zhí)行的同時,其他任務(wù)也可以同時執(zhí)行,不必等待當(dāng)前任務(wù)的完成。大致可以用下圖表示:

我們可以看到,串行的情況下,不同的任務(wù)在任何時間段內(nèi)都不會出現(xiàn)重疊執(zhí)行,而并發(fā)的情況下,不同的任務(wù)在某個時間段內(nèi)可能會存在重疊執(zhí)行。
并發(fā) VS 并行
并發(fā)的概念已經(jīng)介紹過了,并行主要是指某個時刻CPU同時執(zhí)行多個任務(wù)的狀態(tài)。嚴(yán)格意義上來說,多核CPU在不同的核上執(zhí)行不同的任務(wù),是真正的并行,對于單核CPU來說,通過上下文切換來使得不同的任務(wù)在一段時間內(nèi)看起來也是被同時執(zhí)行的。大致可以用下圖表示:

我們可以看到,對于多核CPU,任務(wù)一和任務(wù)二在同時運行,對于單核CPU,任務(wù)一和任務(wù)二在交替運行。當(dāng)然,這里只是舉個例子,并發(fā)代碼(并發(fā)任務(wù))具體是否并行執(zhí)行,完全取決于系統(tǒng)調(diào)度。并行一定需要并發(fā),但并發(fā)不一定會并行。我們能夠決定哪些代碼需要并發(fā),卻不能決定這些代碼真正并行。
串行隊列 VS 并發(fā)隊列
串行隊列中的任務(wù),會按照提交順序一個一個執(zhí)行,一個任務(wù)執(zhí)行完成后,才能執(zhí)行下一個任務(wù);并發(fā)隊列中的任務(wù),也會按照它們被添加的順序執(zhí)行,但完成時機不確定,例如提交了任務(wù)一,再提交了任務(wù)二,并發(fā)隊列只能保證任務(wù)二在任務(wù)一開始執(zhí)行后才執(zhí)行,但任務(wù)二的結(jié)束時間可能比任務(wù)一早,也可能晚。
同步 VS 異步
同步表示當(dāng)前線程會等待已提交的任務(wù)執(zhí)行完成后繼續(xù)往后執(zhí)行,異步表示當(dāng)前線程提交任務(wù)后,直接繼續(xù)往后執(zhí)行,不會等待已提交的任務(wù),已提交的任務(wù)會在稍后的某個時間點完成。同步任務(wù)會阻塞當(dāng)前線程,而異步任務(wù)不會。
死鎖
死鎖表示兩個或多個線程相互等待而導(dǎo)致任何一個線程都不能執(zhí)行。例如線程A等待線程B完成后才執(zhí)行,線程B等待線程A完成后才執(zhí)行,最終結(jié)果是A、B都不能執(zhí)行。死鎖有點類似于OC中的循環(huán)引用,可以對比理解。
上下文切換
上下文切換是指一個線程切換到另外一個線程或進程時保存和恢復(fù)運行狀態(tài)的操作,需要一定的時間和資源開銷。
常見GCD函數(shù)的用法及實例演示
系統(tǒng)隊列類型
系統(tǒng)提供的隊列包括:主隊列(串行隊列)、全局調(diào)度隊列(按優(yōu)先級分為background、low、default和high)、自定義串行隊列、自定義并發(fā)隊列。選擇合適的隊列執(zhí)行合適的任務(wù),是學(xué)習(xí)GCD的重點。注:以下所有示例完整代碼在這里。
dispatch_async
NSString *threadInfo = [NSString stringWithFormat:@"dispatch_async之前線程信息:%@\n\n", [NSThread currentThread]];
[self fillTextInfo:threadInfo];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSThread currentThread] setName:@"dispatch_async demo"];
NSMutableString *resultStr = [NSMutableString string];
[resultStr appendString:[NSString stringWithFormat:@"任務(wù)所在線程信息:%@\n\n", [NSThread currentThread]]];
[resultStr appendString:@"耗時任務(wù)開始執(zhí)行\(zhòng)n\n"];
[NSThread sleepForTimeInterval:3.0];//模擬耗時操作
[resultStr appendString:@"耗時任務(wù)執(zhí)行完畢\n\n"];
//在主線程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf fillTextInfo:resultStr];
});
});
[self fillTextInfo:@"任務(wù)塊后的執(zhí)行代碼\n\n"];
我們通過 dispatch_async 將Block中的代碼(任務(wù))異步提交到優(yōu)先級為Default的全局隊列中執(zhí)行。運行代碼示例后,可以從結(jié)果看出,Block前的代碼執(zhí)行后,立馬執(zhí)行Block后的代碼,并沒有等待Block中的代碼執(zhí)行,在之后的某個時刻,Block中的代碼執(zhí)行完畢,才將更新UI的代碼(任務(wù))提交到了主線程執(zhí)行。下面的表格顯示了不同類型隊列使用dispatch_async的情況:
| 主隊列 | 全局隊列 | 自定義串行隊列 | 自定義并發(fā)隊列 |
|---|---|---|---|
| 如果更新UI的代碼不在主線程上,需要通過dispatch_async提交這些代碼到主隊列,以便在稍后的某個時刻會執(zhí)行這些代碼,在非主線程更新UI會出現(xiàn)不可預(yù)料的bug | 耗時的、非UI操作通過dispatch_async提交到全局隊列是不錯的選擇,全局隊列還包括系統(tǒng)任務(wù),不只是我們自己的任務(wù) | 如果想讓幾個任務(wù)在后臺順序執(zhí)行,可以通過dispatch_async提交到自定義串行列 | 提交的任務(wù)會并發(fā)執(zhí)行,任務(wù)之間可能會同時訪問某一份數(shù)據(jù)而引起數(shù)據(jù)損壞需要注意 |
dispatch_after
[self fillTextInfo:@"準(zhǔn)備執(zhí)行dispatch_after\n\n"];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf fillTextInfo:@"正在執(zhí)行Block\n\n"];
});
[self fillTextInfo:@"dispatch_after后面代碼\n\n"];
上面的dispatch_after在延遲2秒后,將Block任務(wù)異步提交到主隊列中,Block后面的代碼先執(zhí)行,Block里面的代碼后執(zhí)行,即使延遲0秒也是這樣的執(zhí)行順序,因為Block中的代碼會在主隊列中排隊,等到前面的任務(wù)結(jié)束后才會執(zhí)行。 dispatch_after和dispatch_async的區(qū)別只是延遲提交了。兩者大同小異,這里就不詳細(xì)介紹了。
dispatch_sync
NSLog(@"準(zhǔn)備執(zhí)行dispatch_sync");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"正在執(zhí)行Block");
[NSThread sleepForTimeInterval:2.0];//模擬耗時操作
});
NSLog(@"dispatch_sync后面代碼");
上面代碼的執(zhí)行順序是確定的:準(zhǔn)備執(zhí)行dispatch_sync —》正在執(zhí)行Block —》dispatch_sync后面代碼。dispatch_sync使用場景是Block之后的代碼執(zhí)行需要用到Block塊執(zhí)行后的結(jié)果,有前后關(guān)系或者叫依賴。但使用dispatch_sync要注意避免死鎖。下面的代碼就是死鎖:
NSLog(@"準(zhǔn)備執(zhí)行dispatch_sync");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"正在執(zhí)行Block");
[NSThread sleepForTimeInterval:2.0];//模擬耗時操作
});
NSLog(@"dispatch_sync后面代碼");
下面的表格顯示了串行隊列和并發(fā)隊列使用dispatch_sync的注意事項:
| 串行隊列 | 并發(fā)隊列 |
|---|---|
| 如果在某個串行隊列(不管是主隊列還是自定義串行隊列)向本隊列提交了同步任務(wù),一定會產(chǎn)生死鎖,要慎重 | 比較適合使用dispatch_sync |
dispatch_once
單例模式是一種常用的軟件設(shè)計模式。通過單例模式可以保證系統(tǒng)中一個類只有一個實例。在iOS開發(fā)中,dispatch_once是最完美的方案,且效率很高。直接看代碼:
@implementation NBLPhotoManager
+ (instancetype)sharedManager
{
static NBLPhotoManager *_manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_manager = [[self alloc] init];
});
return _manager;
}
@end
for (NSInteger i = 0; i < 5; i++) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NBLPhotoManager *photoManager = [NBLPhotoManager sharedManager];
NSString *tmpStr = [NSString stringWithFormat:@"%@\n\n", photoManager];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf fillTextInfo:tmpStr];
});
});
}
不論創(chuàng)建多少個對象,它們的地址信息都是一樣的,說明是同一個對象。dispatch_once 使Block中的代碼只能執(zhí)行一次,且線程安全。
Dispatch barrier
- (dispatch_queue_t)concurrentQueue
{
if (!_concurrentQueue) {
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.neebel.GCDDemoBarrier", DISPATCH_QUEUE_CONCURRENT);
_concurrentQueue = concurrentQueue;
}
return _concurrentQueue;
}
#pragma mark - Action
- (void)start
{
for (NSInteger i = 0; i < 3; i++) {
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任務(wù)%@", [NSNumber numberWithInteger:i].stringValue);
});
}
dispatch_barrier_async(self.concurrentQueue, ^{
NSLog(@"任務(wù)barrier");
});
for (NSInteger i = 3; i < 6; i++) {
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任務(wù)%@", [NSNumber numberWithInteger:i].stringValue);
});
}
}
代碼中我們提交了三個異步任務(wù)到自定義的并發(fā)隊列中,然后異步提交了一個障礙任務(wù),最后又提交了三個異步任務(wù),從執(zhí)行結(jié)果上可得知,不管前面三個和后面三個任務(wù)各自的執(zhí)行順序如何,障礙任務(wù)總是在前三個任務(wù)執(zhí)行之后執(zhí)行,在后三個任務(wù)執(zhí)行之前執(zhí)行。障礙任務(wù)執(zhí)行期間,其他任務(wù)都不會執(zhí)行,在這段時間內(nèi),相當(dāng)于串行隊列。dispatch_barrier_sync的用法大家自行研究。下面表格顯示了dispatch_barrier的使用場景:
| 串行隊列 | 全局隊列 | 自定義并發(fā)隊列 |
|---|---|---|
| 串行隊列不用dispatch_barrier,因為本來就是串行的 | 最好不要用,會阻塞到系統(tǒng)任務(wù) | 比較適合使用 |
Dispatch group
- (void)startBlockGroup
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[NSThread sleepForTimeInterval:1.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)1完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)2完成");
dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"所有任務(wù)完成");
}
任務(wù)一和二不管完成順序如何,NSLog(@"所有任務(wù)完成") 是在兩個任務(wù)都完成之后才能執(zhí)行。dispatch_group_wait的方式會阻塞當(dāng)前線程。
- (void)startUnBlockGroup
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[NSThread sleepForTimeInterval:1.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)1完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)2完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任務(wù)完成");
});
NSLog(@"非阻塞所以會先打印這句話");
}
上面的方式不會阻塞當(dāng)前線程,所以經(jīng)常會用這種方式。
dispatch_apply
- (void)start
{
dispatch_group_t group = dispatch_group_create();
dispatch_apply(2, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
switch (i) {
case 0:
{
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[NSThread sleepForTimeInterval:1.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)1完成");
dispatch_group_leave(group);
});
}
break;
case 1:
{
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[NSThread sleepForTimeInterval:2.0];//模擬耗時任務(wù),可以調(diào)整時間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)2完成");
dispatch_group_leave(group);
});
}
break;
default:
break;
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任務(wù)完成");
});
}
dispatch_apply類似于for循環(huán),但它的迭代是并發(fā)執(zhí)行的,而for循環(huán)是順序執(zhí)行的。使用時要考慮其資源開銷值不值得。
信號量
信號量機制比較復(fù)雜,用處也很多,例如經(jīng)典的哲學(xué)家進餐問題。深入理解信號量機制需要大家花費更多的時間和精力研究。下面的代碼使用信號量解決線程安全問題,希望能起到拋磚引玉的作用。
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"信號量";
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.startButton];
[self.view addSubview:self.infoTextView];
semaphore = dispatch_semaphore_create(1);
}
- (void)start
{
__weak typeof(self) weakSelf = self;
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(self.concurrentQueue, ^{
NSObject *object = [weakSelf buildAnObj];
NSLog(@"%@", object);
});
}
for (NSInteger i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSObject *object = [weakSelf buildAnObj];
NSLog(@"%@", object);
});
}
}
//目的是只創(chuàng)建一個對象
- (NSObject *)buildAnObj
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!self.obj) {//這個判斷在多線程訪問時是不安全的,可能存在多個線程同時進入執(zhí)行的情況,使用信號量機制充當(dāng)鎖就沒問題了
self.obj = [[NSObject alloc] init];
}
dispatch_semaphore_signal(semaphore);
return self.obj;
}
上面的代碼無論執(zhí)行多少次都只會創(chuàng)建一個對象,原因是dispatch_semaphore_create(1),創(chuàng)建了一個值為1的信號量,當(dāng)一個線程A執(zhí)行了dispatch_semaphore_wait后,信號量的值會減1,變?yōu)?,這時候其他線程就會等待,等到A執(zhí)行dispatch_semaphore_signal后,信號量才會加1,其他線程才會繼續(xù)執(zhí)行,作用類似于線程鎖,從而保證了線程安全。
至此,iOS多線程之GCD就介紹完了,文中沒有用到很多專業(yè)解釋,都是根據(jù)自己的理解表述的,目的是便于大家理解,有不妥或不正確的還請指正。