本篇文章是iOS多線程系列的第二篇文章,之所以將GCD放在第二篇介紹,是因為理解了GCD后就比較容易理解NSOperation,NSOperation是蘋果對GCD的封裝的產物,以便我們開發中更好地使用。
本篇文章主要內容:
- GCD是什么
- 學習GCD之前需要理解的東西
- 常見GCD函數的用法及實例演示
GCD是什么
GCD(Grand Central Dispatch),是libdispatch的市場名稱,是Apple開發的一個多線程編程的優化方案,Apple也推薦開發者使用此方案。libdispatch基于線程池的模式管理、執行并行任務。GCD大大降低了開發者維護線程的成本,使用起來也更加便捷、愉悅。
學習GCD之前需要理解的東西
俗話說:磨刀不誤砍柴工,理解多線程的相關知識對于深入學習GCD是十分必要的。作為計算機相關專業的學生,多線程的學習是基礎同時也是必不可少,其內容豐富,本篇文章只做大致回顧,不再深入探討。注:以下術語及概念為個人理解表述
串行 VS 并發
串行和并發,是用來描述任務與任務之間的相對關系。串行指的是當前任務執行時其他任務需要等待,等待當前任務完成后,才能繼續下一個任務,任務一個一個地執行;并發指的是當前任務執行的同時,其他任務也可以同時執行,不必等待當前任務的完成。大致可以用下圖表示:
我們可以看到,串行的情況下,不同的任務在任何時間段內都不會出現重疊執行,而并發的情況下,不同的任務在某個時間段內可能會存在重疊執行。
并發 VS 并行
并發的概念已經介紹過了,并行主要是指某個時刻CPU同時執行多個任務的狀態。嚴格意義上來說,多核CPU在不同的核上執行不同的任務,是真正的并行,對于單核CPU來說,通過上下文切換來使得不同的任務在一段時間內看起來也是被同時執行的。大致可以用下圖表示:
我們可以看到,對于多核CPU,任務一和任務二在同時運行,對于單核CPU,任務一和任務二在交替運行。當然,這里只是舉個例子,并發代碼(并發任務)具體是否并行執行,完全取決于系統調度。并行一定需要并發,但并發不一定會并行。我們能夠決定哪些代碼需要并發,卻不能決定這些代碼真正并行。
串行隊列 VS 并發隊列
串行隊列中的任務,會按照提交順序一個一個執行,一個任務執行完成后,才能執行下一個任務;并發隊列中的任務,也會按照它們被添加的順序執行,但完成時機不確定,例如提交了任務一,再提交了任務二,并發隊列只能保證任務二在任務一開始執行后才執行,但任務二的結束時間可能比任務一早,也可能晚。
同步 VS 異步
同步表示當前線程會等待已提交的任務執行完成后繼續往后執行,異步表示當前線程提交任務后,直接繼續往后執行,不會等待已提交的任務,已提交的任務會在稍后的某個時間點完成。同步任務會阻塞當前線程,而異步任務不會。
死鎖
死鎖表示兩個或多個線程相互等待而導致任何一個線程都不能執行。例如線程A等待線程B完成后才執行,線程B等待線程A完成后才執行,最終結果是A、B都不能執行。死鎖有點類似于OC中的循環引用,可以對比理解。
上下文切換
上下文切換是指一個線程切換到另外一個線程或進程時保存和恢復運行狀態的操作,需要一定的時間和資源開銷。
常見GCD函數的用法及實例演示
系統隊列類型
系統提供的隊列包括:主隊列(串行隊列)、全局調度隊列(按優先級分為background、low、default和high)、自定義串行隊列、自定義并發隊列。選擇合適的隊列執行合適的任務,是學習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:@"任務所在線程信息:%@\n\n", [NSThread currentThread]]];
[resultStr appendString:@"耗時任務開始執行\n\n"];
[NSThread sleepForTimeInterval:3.0];//模擬耗時操作
[resultStr appendString:@"耗時任務執行完畢\n\n"];
//在主線程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf fillTextInfo:resultStr];
});
});
[self fillTextInfo:@"任務塊后的執行代碼\n\n"];
我們通過 dispatch_async 將Block中的代碼(任務)異步提交到優先級為Default的全局隊列中執行。運行代碼示例后,可以從結果看出,Block前的代碼執行后,立馬執行Block后的代碼,并沒有等待Block中的代碼執行,在之后的某個時刻,Block中的代碼執行完畢,才將更新UI的代碼(任務)提交到了主線程執行。下面的表格顯示了不同類型隊列使用dispatch_async的情況:
主隊列 | 全局隊列 | 自定義串行隊列 | 自定義并發隊列 |
---|---|---|---|
如果更新UI的代碼不在主線程上,需要通過dispatch_async提交這些代碼到主隊列,以便在稍后的某個時刻會執行這些代碼,在非主線程更新UI會出現不可預料的bug | 耗時的、非UI操作通過dispatch_async提交到全局隊列是不錯的選擇,全局隊列還包括系統任務,不只是我們自己的任務 | 如果想讓幾個任務在后臺順序執行,可以通過dispatch_async提交到自定義串行列 | 提交的任務會并發執行,任務之間可能會同時訪問某一份數據而引起數據損壞需要注意 |
dispatch_after
[self fillTextInfo:@"準備執行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:@"正在執行Block\n\n"];
});
[self fillTextInfo:@"dispatch_after后面代碼\n\n"];
上面的dispatch_after在延遲2秒后,將Block任務異步提交到主隊列中,Block后面的代碼先執行,Block里面的代碼后執行,即使延遲0秒也是這樣的執行順序,因為Block中的代碼會在主隊列中排隊,等到前面的任務結束后才會執行。 dispatch_after和dispatch_async的區別只是延遲提交了。兩者大同小異,這里就不詳細介紹了。
dispatch_sync
NSLog(@"準備執行dispatch_sync");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"正在執行Block");
[NSThread sleepForTimeInterval:2.0];//模擬耗時操作
});
NSLog(@"dispatch_sync后面代碼");
上面代碼的執行順序是確定的:準備執行dispatch_sync —》正在執行Block —》dispatch_sync后面代碼。dispatch_sync使用場景是Block之后的代碼執行需要用到Block塊執行后的結果,有前后關系或者叫依賴。但使用dispatch_sync要注意避免死鎖。下面的代碼就是死鎖:
NSLog(@"準備執行dispatch_sync");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"正在執行Block");
[NSThread sleepForTimeInterval:2.0];//模擬耗時操作
});
NSLog(@"dispatch_sync后面代碼");
下面的表格顯示了串行隊列和并發隊列使用dispatch_sync的注意事項:
串行隊列 | 并發隊列 |
---|---|
如果在某個串行隊列(不管是主隊列還是自定義串行隊列)向本隊列提交了同步任務,一定會產生死鎖,要慎重 | 比較適合使用dispatch_sync |
dispatch_once
單例模式是一種常用的軟件設計模式。通過單例模式可以保證系統中一個類只有一個實例。在iOS開發中,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];
});
});
}
不論創建多少個對象,它們的地址信息都是一樣的,說明是同一個對象。dispatch_once 使Block中的代碼只能執行一次,且線程安全。
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(@"任務%@", [NSNumber numberWithInteger:i].stringValue);
});
}
dispatch_barrier_async(self.concurrentQueue, ^{
NSLog(@"任務barrier");
});
for (NSInteger i = 3; i < 6; i++) {
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任務%@", [NSNumber numberWithInteger:i].stringValue);
});
}
}
代碼中我們提交了三個異步任務到自定義的并發隊列中,然后異步提交了一個障礙任務,最后又提交了三個異步任務,從執行結果上可得知,不管前面三個和后面三個任務各自的執行順序如何,障礙任務總是在前三個任務執行之后執行,在后三個任務執行之前執行。障礙任務執行期間,其他任務都不會執行,在這段時間內,相當于串行隊列。dispatch_barrier_sync的用法大家自行研究。下面表格顯示了dispatch_barrier的使用場景:
串行隊列 | 全局隊列 | 自定義并發隊列 |
---|---|---|
串行隊列不用dispatch_barrier,因為本來就是串行的 | 最好不要用,會阻塞到系統任務 | 比較適合使用 |
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];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務1完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務2完成");
dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"所有任務完成");
}
任務一和二不管完成順序如何,NSLog(@"所有任務完成") 是在兩個任務都完成之后才能執行。dispatch_group_wait的方式會阻塞當前線程。
- (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];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務1完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2.0];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務2完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任務完成");
});
NSLog(@"非阻塞所以會先打印這句話");
}
上面的方式不會阻塞當前線程,所以經常會用這種方式。
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];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務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];//模擬耗時任務,可以調整時間模擬任務一和二的完成順序
NSLog(@"任務2完成");
dispatch_group_leave(group);
});
}
break;
default:
break;
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"所有任務完成");
});
}
dispatch_apply類似于for循環,但它的迭代是并發執行的,而for循環是順序執行的。使用時要考慮其資源開銷值不值得。
信號量
信號量機制比較復雜,用處也很多,例如經典的哲學家進餐問題。深入理解信號量機制需要大家花費更多的時間和精力研究。下面的代碼使用信號量解決線程安全問題,希望能起到拋磚引玉的作用。
- (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);
});
}
}
//目的是只創建一個對象
- (NSObject *)buildAnObj
{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (!self.obj) {//這個判斷在多線程訪問時是不安全的,可能存在多個線程同時進入執行的情況,使用信號量機制充當鎖就沒問題了
self.obj = [[NSObject alloc] init];
}
dispatch_semaphore_signal(semaphore);
return self.obj;
}
上面的代碼無論執行多少次都只會創建一個對象,原因是dispatch_semaphore_create(1),創建了一個值為1的信號量,當一個線程A執行了dispatch_semaphore_wait后,信號量的值會減1,變為0,這時候其他線程就會等待,等到A執行dispatch_semaphore_signal后,信號量才會加1,其他線程才會繼續執行,作用類似于線程鎖,從而保證了線程安全。
至此,iOS多線程之GCD就介紹完了,文中沒有用到很多專業解釋,都是根據自己的理解表述的,目的是便于大家理解,有不妥或不正確的還請指正。