本篇文章是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就介绍完了,文中没有用到很多专业解释,都是根据自己的理解表述的,目的是便于大家理解,有不妥或不正确的还请指正。