本篇文章是iOS多線程系列的第二篇文章责掏,之所以將GCD放在第二篇介紹,是因?yàn)槔斫饬薌CD后就比較容易理解NSOperation湃望,NSOperation是蘋果對(duì)GCD的封裝的產(chǎn)物换衬,以便我們開發(fā)中更好地使用。
本篇文章主要內(nèi)容:
- GCD是什么
- 學(xué)習(xí)GCD之前需要理解的東西
- 常見GCD函數(shù)的用法及實(shí)例演示
GCD是什么
GCD(Grand Central Dispatch)证芭,是libdispatch的市場(chǎng)名稱瞳浦,是Apple開發(fā)的一個(gè)多線程編程的優(yōu)化方案,Apple也推薦開發(fā)者使用此方案废士。libdispatch基于線程池的模式管理叫潦、執(zhí)行并行任務(wù)。GCD大大降低了開發(fā)者維護(hù)線程的成本官硝,使用起來也更加便捷矗蕊、愉悅四敞。
學(xué)習(xí)GCD之前需要理解的東西
俗話說:磨刀不誤砍柴工,理解多線程的相關(guān)知識(shí)對(duì)于深入學(xué)習(xí)GCD是十分必要的拔妥。作為計(jì)算機(jī)相關(guān)專業(yè)的學(xué)生忿危,多線程的學(xué)習(xí)是基礎(chǔ)同時(shí)也是必不可少,其內(nèi)容豐富没龙,本篇文章只做大致回顧铺厨,不再深入探討。注:以下術(shù)語及概念為個(gè)人理解表述
串行 VS 并發(fā)
串行和并發(fā)硬纤,是用來描述任務(wù)與任務(wù)之間的相對(duì)關(guān)系解滓。串行指的是當(dāng)前任務(wù)執(zhí)行時(shí)其他任務(wù)需要等待,等待當(dāng)前任務(wù)完成后筝家,才能繼續(xù)下一個(gè)任務(wù)洼裤,任務(wù)一個(gè)一個(gè)地執(zhí)行;并發(fā)指的是當(dāng)前任務(wù)執(zhí)行的同時(shí)溪王,其他任務(wù)也可以同時(shí)執(zhí)行腮鞍,不必等待當(dāng)前任務(wù)的完成。大致可以用下圖表示:
我們可以看到莹菱,串行的情況下移国,不同的任務(wù)在任何時(shí)間段內(nèi)都不會(huì)出現(xiàn)重疊執(zhí)行,而并發(fā)的情況下道伟,不同的任務(wù)在某個(gè)時(shí)間段內(nèi)可能會(huì)存在重疊執(zhí)行迹缀。
并發(fā) VS 并行
并發(fā)的概念已經(jīng)介紹過了,并行主要是指某個(gè)時(shí)刻CPU同時(shí)執(zhí)行多個(gè)任務(wù)的狀態(tài)蜜徽。嚴(yán)格意義上來說祝懂,多核CPU在不同的核上執(zhí)行不同的任務(wù),是真正的并行拘鞋,對(duì)于單核CPU來說砚蓬,通過上下文切換來使得不同的任務(wù)在一段時(shí)間內(nèi)看起來也是被同時(shí)執(zhí)行的。大致可以用下圖表示:
我們可以看到掐禁,對(duì)于多核CPU怜械,任務(wù)一和任務(wù)二在同時(shí)運(yùn)行,對(duì)于單核CPU傅事,任務(wù)一和任務(wù)二在交替運(yùn)行缕允。當(dāng)然,這里只是舉個(gè)例子蹭越,并發(fā)代碼(并發(fā)任務(wù))具體是否并行執(zhí)行障本,完全取決于系統(tǒng)調(diào)度。并行一定需要并發(fā),但并發(fā)不一定會(huì)并行驾霜。我們能夠決定哪些代碼需要并發(fā)案训,卻不能決定這些代碼真正并行。
串行隊(duì)列 VS 并發(fā)隊(duì)列
串行隊(duì)列中的任務(wù)粪糙,會(huì)按照提交順序一個(gè)一個(gè)執(zhí)行强霎,一個(gè)任務(wù)執(zhí)行完成后,才能執(zhí)行下一個(gè)任務(wù)蓉冈;并發(fā)隊(duì)列中的任務(wù)城舞,也會(huì)按照它們被添加的順序執(zhí)行,但完成時(shí)機(jī)不確定寞酿,例如提交了任務(wù)一家夺,再提交了任務(wù)二,并發(fā)隊(duì)列只能保證任務(wù)二在任務(wù)一開始執(zhí)行后才執(zhí)行伐弹,但任務(wù)二的結(jié)束時(shí)間可能比任務(wù)一早拉馋,也可能晚。
同步 VS 異步
同步表示當(dāng)前線程會(huì)等待已提交的任務(wù)執(zhí)行完成后繼續(xù)往后執(zhí)行惨好,異步表示當(dāng)前線程提交任務(wù)后煌茴,直接繼續(xù)往后執(zhí)行,不會(huì)等待已提交的任務(wù)昧狮,已提交的任務(wù)會(huì)在稍后的某個(gè)時(shí)間點(diǎn)完成景馁。同步任務(wù)會(huì)阻塞當(dāng)前線程,而異步任務(wù)不會(huì)逗鸣。
死鎖
死鎖表示兩個(gè)或多個(gè)線程相互等待而導(dǎo)致任何一個(gè)線程都不能執(zhí)行。例如線程A等待線程B完成后才執(zhí)行绰精,線程B等待線程A完成后才執(zhí)行撒璧,最終結(jié)果是A、B都不能執(zhí)行笨使。死鎖有點(diǎn)類似于OC中的循環(huán)引用卿樱,可以對(duì)比理解。
上下文切換
上下文切換是指一個(gè)線程切換到另外一個(gè)線程或進(jìn)程時(shí)保存和恢復(fù)運(yùn)行狀態(tài)的操作硫椰,需要一定的時(shí)間和資源開銷繁调。
常見GCD函數(shù)的用法及實(shí)例演示
系統(tǒng)隊(duì)列類型
系統(tǒng)提供的隊(duì)列包括:主隊(duì)列(串行隊(duì)列)、全局調(diào)度隊(duì)列(按優(yōu)先級(jí)分為background靶草、low蹄胰、default和high)、自定義串行隊(duì)列奕翔、自定義并發(fā)隊(duì)列裕寨。選擇合適的隊(duì)列執(zhí)行合適的任務(wù),是學(xué)習(xí)GCD的重點(diǎn)。注:以下所有示例完整代碼在這里宾袜。
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:@"耗時(shí)任務(wù)開始執(zhí)行\(zhòng)n\n"];
[NSThread sleepForTimeInterval:3.0];//模擬耗時(shí)操作
[resultStr appendString:@"耗時(shí)任務(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)先級(jí)為Default的全局隊(duì)列中執(zhí)行捻艳。運(yùn)行代碼示例后,可以從結(jié)果看出庆猫,Block前的代碼執(zhí)行后认轨,立馬執(zhí)行Block后的代碼,并沒有等待Block中的代碼執(zhí)行月培,在之后的某個(gè)時(shí)刻嘁字,Block中的代碼執(zhí)行完畢,才將更新UI的代碼(任務(wù))提交到了主線程執(zhí)行节视。下面的表格顯示了不同類型隊(duì)列使用dispatch_async的情況:
主隊(duì)列 | 全局隊(duì)列 | 自定義串行隊(duì)列 | 自定義并發(fā)隊(duì)列 |
---|---|---|---|
如果更新UI的代碼不在主線程上拳锚,需要通過dispatch_async提交這些代碼到主隊(duì)列,以便在稍后的某個(gè)時(shí)刻會(huì)執(zhí)行這些代碼寻行,在非主線程更新UI會(huì)出現(xiàn)不可預(yù)料的bug | 耗時(shí)的霍掺、非UI操作通過dispatch_async提交到全局隊(duì)列是不錯(cuò)的選擇,全局隊(duì)列還包括系統(tǒng)任務(wù)拌蜘,不只是我們自己的任務(wù) | 如果想讓幾個(gè)任務(wù)在后臺(tái)順序執(zhí)行杆烁,可以通過dispatch_async提交到自定義串行列 | 提交的任務(wù)會(huì)并發(fā)執(zhí)行,任務(wù)之間可能會(huì)同時(shí)訪問某一份數(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ù)異步提交到主隊(duì)列中兔魂,Block后面的代碼先執(zhí)行,Block里面的代碼后執(zhí)行举娩,即使延遲0秒也是這樣的執(zhí)行順序析校,因?yàn)锽lock中的代碼會(huì)在主隊(duì)列中排隊(duì),等到前面的任務(wù)結(jié)束后才會(huì)執(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];//模擬耗時(shí)操作
});
NSLog(@"dispatch_sync后面代碼");
上面代碼的執(zhí)行順序是確定的:準(zhǔn)備執(zhí)行dispatch_sync —》正在執(zhí)行Block —》dispatch_sync后面代碼吊奢。dispatch_sync使用場(chǎng)景是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];//模擬耗時(shí)操作
});
NSLog(@"dispatch_sync后面代碼");
下面的表格顯示了串行隊(duì)列和并發(fā)隊(duì)列使用dispatch_sync的注意事項(xiàng):
串行隊(duì)列 | 并發(fā)隊(duì)列 |
---|---|
如果在某個(gè)串行隊(duì)列(不管是主隊(duì)列還是自定義串行隊(duì)列)向本隊(duì)列提交了同步任務(wù),一定會(huì)產(chǎn)生死鎖铺呵,要慎重 | 比較適合使用dispatch_sync |
dispatch_once
單例模式是一種常用的軟件設(shè)計(jì)模式裹驰。通過單例模式可以保證系統(tǒng)中一個(gè)類只有一個(gè)實(shí)例。在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)建多少個(gè)對(duì)象,它們的地址信息都是一樣的滋将,說明是同一個(gè)對(duì)象邻悬。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);
});
}
}
代碼中我們提交了三個(gè)異步任務(wù)到自定義的并發(fā)隊(duì)列中父丰,然后異步提交了一個(gè)障礙任務(wù),最后又提交了三個(gè)異步任務(wù)掘宪,從執(zhí)行結(jié)果上可得知蛾扇,不管前面三個(gè)和后面三個(gè)任務(wù)各自的執(zhí)行順序如何,障礙任務(wù)總是在前三個(gè)任務(wù)執(zhí)行之后執(zhí)行魏滚,在后三個(gè)任務(wù)執(zhí)行之前執(zhí)行镀首。障礙任務(wù)執(zhí)行期間,其他任務(wù)都不會(huì)執(zhí)行鼠次,在這段時(shí)間內(nèi)更哄,相當(dāng)于串行隊(duì)列。dispatch_barrier_sync的用法大家自行研究腥寇。下面表格顯示了dispatch_barrier的使用場(chǎng)景:
串行隊(duì)列 | 全局隊(duì)列 | 自定義并發(fā)隊(duì)列 |
---|---|---|
串行隊(duì)列不用dispatch_barrier成翩,因?yàn)楸緛砭褪谴械?/td> | 最好不要用,會(huì)阻塞到系統(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];//模擬耗時(shí)任務(wù)赦役,可以調(diào)整時(shí)間模擬任務(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];//模擬耗時(shí)任務(wù)麻敌,可以調(diào)整時(shí)間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)2完成");
dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"所有任務(wù)完成");
}
任務(wù)一和二不管完成順序如何,NSLog(@"所有任務(wù)完成") 是在兩個(gè)任務(wù)都完成之后才能執(zhí)行掂摔。dispatch_group_wait的方式會(huì)阻塞當(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];//模擬耗時(shí)任務(wù),可以調(diào)整時(shí)間模擬任務(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];//模擬耗時(shí)任務(wù)乙漓,可以調(diào)整時(shí)間模擬任務(wù)一和二的完成順序
NSLog(@"任務(wù)2完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任務(wù)完成");
});
NSLog(@"非阻塞所以會(huì)先打印這句話");
}
上面的方式不會(huì)阻塞當(dāng)前線程聂示,所以經(jīng)常會(huì)用這種方式。
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];//模擬耗時(shí)任務(wù)簇秒,可以調(diào)整時(shí)間模擬任務(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];//模擬耗時(shí)任務(wù)妇押,可以調(diào)整時(shí)間模擬任務(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í)行的烁落。使用時(shí)要考慮其資源開銷值不值得捉片。
信號(hào)量
信號(hào)量機(jī)制比較復(fù)雜歌馍,用處也很多肢簿,例如經(jīng)典的哲學(xué)家進(jìn)餐問題僧凤。深入理解信號(hào)量機(jī)制需要大家花費(fèi)更多的時(shí)間和精力研究豆巨。下面的代碼使用信號(hào)量解決線程安全問題剩辟,希望能起到拋磚引玉的作用。
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"信號(hào)量";
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)建一個(gè)對(duì)象
- (NSObject *)buildAnObj
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!self.obj) {//這個(gè)判斷在多線程訪問時(shí)是不安全的,可能存在多個(gè)線程同時(shí)進(jìn)入執(zhí)行的情況贩猎,使用信號(hào)量機(jī)制充當(dāng)鎖就沒問題了
self.obj = [[NSObject alloc] init];
}
dispatch_semaphore_signal(semaphore);
return self.obj;
}
上面的代碼無論執(zhí)行多少次都只會(huì)創(chuàng)建一個(gè)對(duì)象熊户,原因是dispatch_semaphore_create(1),創(chuàng)建了一個(gè)值為1的信號(hào)量吭服,當(dāng)一個(gè)線程A執(zhí)行了dispatch_semaphore_wait后嚷堡,信號(hào)量的值會(huì)減1,變?yōu)?艇棕,這時(shí)候其他線程就會(huì)等待蝌戒,等到A執(zhí)行dispatch_semaphore_signal后,信號(hào)量才會(huì)加1沼琉,其他線程才會(huì)繼續(xù)執(zhí)行北苟,作用類似于線程鎖,從而保證了線程安全打瘪。
至此友鼻,iOS多線程之GCD就介紹完了,文中沒有用到很多專業(yè)解釋瑟慈,都是根據(jù)自己的理解表述的桃移,目的是便于大家理解,有不妥或不正確的還請(qǐng)指正葛碧。