GCD的使用技巧
多线程编程相关概念
串行(Serial)与并发(Concurrent)
这两个词都是形容当执行任务时是否需要考虑其它任务。
串行:任务只能一个接着一个地执行
并发:同一时间内,多个任务可以是同时执行的
同步(Synchronous)与异步(Asychronous)
这两个词是用来形容,被调用的函数在什么时候将控制权返回给调用者。
同步:被调用的函数只会在执行完才将控制权给回调用者。
异步:被调用的函数会马上将控制权返回给调用者,不管函数是否执行完。因此异步函数不会阻塞当前进程。
线程安全
能被多个线程同时执行的资源,且不会出现资源竞争、死锁、顺序错乱、优先级翻转等问题
危险代码段(Critical Section)
不能被并发执行的代码,不能被两个线程同时执行,比如因为修改共享的资源或者变量,只要运行就会立即崩溃或者是出错
资源竞争
如图,有一个变量存储着17,然后线程A读取变量得到17,线程B也读取了这个变量。两个线程都进行了加1运算得,并写入18都变量。从而导致了崩溃。这就是race condition,多线程使用共享资源,而没有确保其它线程是已经结束使用共享资源
互相排除
如图,使用锁对线程正在使用的共享资源进行锁定,当共享资源使用完后。其它线程才能使用这个共享变量。这样就能避免race condition但会导致死锁。
死锁(Deadlock)
两个线程等待着彼此的完成而陷入的困境称为死锁。
void swap(A, B)
{
lock(lockA);
lock(lockB);
int a = A;
int b = B;
A = b;
B = a;
unlock(lockB);
unlock(lockA);
}
swap(X, Y);
// 线程 thread 1
swap(Y, X);
// 线程 thread 2
结果就会导致X被线程1锁住,Y被线程2锁住。而线程1又不能使用Y直到线程2解锁,同理,线程2也不能使用X。这就是死锁,互相等待。
优先顺序颠倒(Priority Inversion)
优先顺序颠倒问题,是由低权限任务的阻塞着高权限任务的执行。从而让顺序颠倒。
假设低权限的线程和高权限的线程使用共享资源。本应该,低权限的线程任务使用完共享资源后高权限的线程任务就能没有延迟地执行。
但由于一开始高权限的线程因为低线程的锁而受到阻塞。所以就给了机会中权限的线程任务,因为现在高权限受阻,所以中权限的线程任务是权限最高的,所以中权限任务中断低权限的线程任务的执行。从而让低线程的锁解不开,高线程任务也就延迟执行。从而优先顺序颠倒。
所以在使用GCD的时候,最好将多线程的任务执行优先权限保持一致。
苹果工程师在swift-dev邮件列表中讨论weak属性的线程安全问题的邮件里爆出自旋锁有bug,邮件地址:https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000372.html。大概就是不同优先级线程调度算法会有优先级反转问题,比如低优先级获锁访问资源,高优先级尝试访问时会等待,这时低优先级又没法争过高优先级导致任务无法完成lock释放不了。
GCD的队列类型
QOS服务等级
队列服务等级指的是优先级的抽象,针对的是在系统资源出现竞争或者是高性能负荷的情况下,根据服务等级动态调整资源分配,对于使用GCD的开发者来说,预先将程序中的队列进行服务等级的划分和管理,将需要进行执行的任务按照服务层次划分,则可以更好的利用系统智能调度资源的特性。比如,响应用户对请求的任务、无须等待的后台任务、需要长时间且高性能保证的任务等等。
//创建QOS服务等级的队列
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INTERACTIVE, QOS_MIN_RELATIVE_PRIORITY);
dispatch_queue_t concurrent_queue = dispatch_queue_create("concurrent_queue.com", attr);
dispatch_async(concurrent_queue, ^{
NSLog(@"concurrent_queue task exc");
});
QOS服务等级如下:
/*
QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
QOS_CLASS_USER_INITIATED:user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
QOS_CLASS_UTILITY:utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
QOS_CLASS_BACKGROUND:background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。
*/
__QOS_ENUM(qos_class, unsigned int,
QOS_CLASS_USER_INTERACTIVE
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x21,
QOS_CLASS_USER_INITIATED
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x19,
QOS_CLASS_DEFAULT
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x15,
QOS_CLASS_UTILITY
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x11,
QOS_CLASS_BACKGROUND
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x09,
QOS_CLASS_UNSPECIFIED
__QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x00,
);
从服务类型这个维度区分,可能会将本应该串行执行或者是并行执行的任务,却因服务类型不同而切分不同的队列,比如,一个网络请求任务,一个相应用户事件的任务,本应该两个任务是串行执行的,但划分了不同的QOS,导致放在两个串行队列中执行,这个两个任务就成并行任务了,解决这个问题可以使用设置目标队列。
设置目标队列,划分队列层次
dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);
//这三个队列可标记服务层级
dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"1 in");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"1 out");
});
dispatch_async(queue2, ^{
NSLog(@"2 in");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"2 out");
});
dispatch_async(queue3, ^{
NSLog(@"3 in");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"3 out");
});
各种队列使用的情况
- dispatch_get_main_queue()-主队列(串行队列):需要异步向UI主线程提交任务或队列中有任务完成需要更新UI时,dispatch_after在这种类型中使用,不能同步提交或者是提交阻塞任务。
- 自定义顺序队列:顺序执行后台任务并追踪它时。这样做同时只有一个任务在执行可以防止资源竞争。dipatch barriers解决读写锁问题的放在这里处理。dispatch groups也是放在这里。
- 并发队列:用来执行与UI无关的后台任务,dispatch_sync放在这里,方便等待任务完成进行后续处理或和dispatch barrier同步。dispatch groups放在这里也不错。
切莫滥用dispatch_apply
dispatch_apply(100000, dispatch_queue_create(DISPATCH_CURRENT_QUEUE_LABEL, NULL), ^(size_t i) {
NSLog(@"%ld",i);
});
dispatch_apply类似于for循环,循环体的逻辑可放在并发队列中并行执行,可加快for循环速度,但切莫滥用,因为GCD理论上可并发创建64条线程,当并发队列执行较多任务时,dispatch_apply提交到并发队列的动作则会阻塞其所在线程。
使用串行同步队列保证数据同步
-(NSString *)name{
__block NSString * localName ;
dispatch_sync(_t, ^{
localName = _name;
});
return localName;
}
-(void)setName:(NSString *)name{
dispatch_sync(_t, ^{
_name = name;
});
}
self.t =dispatch_queue_create("com.topic-gcd.com", DISPATCH_QUEUE_SERIAL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
self.name = @"111";
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"%@",self.name);
});
使用串行异步队列保证数据同步
- (NSString *)name1{
__block NSString * localName1;
dispatch_sync(_t, ^{
for (int i = 0; i<10000; i++) {
}
localName1 = _name1;
});
return localName1;
}
-(void)setName1:(NSString *)name1{
dispatch_async(_t, ^{
_name1 = name1;
});
}
NSDate * date = [NSDate date];
self.name = @"222";
NSLog(@"%@",self.name);
NSLog(@"name----%f",[NSDate date].timeIntervalSinceNow - date.timeIntervalSinceNow);
date = [NSDate date];
self.name1 = @"111";
NSLog(@"%@",self.name1);
NSLog(@"name1----%f",[NSDate date].timeIntervalSinceNow - date.timeIntervalSinceNow);
结果如下:
2017-04-11 13:33:45.512 topic-gcd[52228:4492576] 222
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] name----0.000390
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] 111
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] name1----0.000198
使用sync(read)和barrier_async(write)并利用并发队列,执行数据读写任务,确保read、write有序,防止多线程中资源死锁。
- (void)setName2:(NSString *)name2{
dispatch_barrier_async(self.t, ^{
_name2 = name2;
});
}
- (NSString *)name2{
__block NSString * localName2;
dispatch_sync(self.t, ^{
for (int i = 0; i<1000000; i++) {
}
localName2 = _name2;
});
return localName2;
}
//使用并行队列,读操作异步执行,写操作同步执行
self.t = dispatch_queue_create(DISPATCH_CURRENT_QUEUE_LABEL, NULL);
self.name2 = @"33";
NSLog(@"1.read name2---->%@",self.name2);
NSLog(@"2.read name2---->%@",self.name2);
NSLog(@"3.read name2---->%@",self.name2);
self.name2 = @"333";
NSLog(@"4.read name2---->%@",self.name2);
NSLog(@"5.read name2---->%@",self.name2);
NSLog(@"6.read name2---->%@",self.name2);
dispatch_barrier_async为什么适合使用在自定义并发队列呢?
- 串行队列本来就是一个任务一个任务地串行执行,没有意义
- 全局队列(Global Concurrent Queue),因为全局队列很多地方在用,包括系统的API也在用,使用dispatch_barrier_async可以阻塞此队列,所以不建议使用
- 自定义并发队列,只要并发队列里使用的是资源线程安全的。所以比较建议
使用Dispatch Group,将不同队列进行分组,监听或等待同一组的队列任务
Dispatch Group将不同队列设置为一个group,可以利用dispatch_group_wait设置阻塞点,等待整个组内的异步任务完成,或者是使用dispatch_group_notify异步执行完成组内任务的回调,从而不阻塞当前线程。注意,group中如果添加主队列再使用dispatch_group_wait有可能引起死锁。
//使用Dispatch Group,并发队列异步执行任务
dispatch_group_t group = dispatch_group_create();
//在主线程队列中执行
dispatch_group_async(group, dispatch_get_main_queue(), ^{
NSLog(@"任务1执行");
});
//在创建的一个并行队列中执行
dispatch_group_async(group, self.t, ^{
NSLog(@"任务2执行");
});
//在全局队列中执行
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务3执行");
});
//死锁
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// NSLog(@"任务4执行");
//在主线程队列中监听所有任务执行完毕的回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"任务5执行");
});
使用dispatch_group_enter与dispatch_group_leave合并异步任务组
对于某些异步任务,想要利用任务组来管理达到监听完成回调或者是阻塞当前线程进行线程同步,则可以使用dispatch_group_enter与dispatch_group_leave,例子如下:
//使用dispatch_group_enter与dispatch_group_leave合并异步任务组
dispatch_group_t block_group = dispatch_group_create();
dispatch_group_enter(block_group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"异步任务1");
dispatch_group_leave(block_group);
});
dispatch_group_enter(block_group);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"异步任务2");
dispatch_group_leave(block_group);
});
dispatch_group_notify(block_group, dispatch_get_main_queue(), ^{
NSLog(@"异步任务1和任务2全部完成");
});
虽然异步任务1与异步任务2是不同类型的队列里面的任务,但可以通过enter与leave来达到将两个任务进行任务组编排,比方说一个界面需要等待三个接口调用全部完成才能够初始化,除了以前设置哨兵变量以外,还可以使用上面的方式来进行任务编排。在使用CoreData的时候就可以将异步数据库操作形成任务组。
dispatch_group_enter(group);
[self.context performBlock:^(){
//some core data actions
dispatch_group_leave(group);
}];
需要注意的是:
dispatch_group_async等价于dispatch_group_enter() 和 dispatch_group_leave()的组合。
dispatch_group_enter() 必须运行在 dispatch_group_leave() 之前。
dispatch_group_enter() 和 dispatch_group_leave() 需要成对出现的
使用dispatch_block_wait与dispatch_block_notify来设置任务block的异步阻塞或者是异步完成回调监听
//dispatch_block_wait使用
dispatch_queue_t block_serialQueue = dispatch_queue_create("com.block_serialQueue.com", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block = dispatch_block_create(0, ^{
[NSThread sleepForTimeInterval:5.f];
NSLog(@"block_serialQueue block end");
});
dispatch_async(block_serialQueue, block);
//设置DISPATCH_TIME_FOREVER会一直等到前面任务都完成
dispatch_block_wait(block, DISPATCH_TIME_FOREVER);
block = dispatch_block_create(0, ^{
NSLog(@"second block_serialQueue block end");
});
dispatch_async(block_serialQueue, block);
dispatch_block_notify(block, dispatch_get_main_queue(), ^{
NSLog(@"block_serialQueue block finished");
});
当某一个异步任务block,需要监听其是否完成,可以使用wait与notify来简化代码逻辑。例如,可使用该特性,来组合异步任务的执行顺序呢,主要是针对已存在的异步任务逻辑代码。比方说,可将网络请求的异步任务进行串联。
使用Dispatch Group与dispatch_block_cancel,取消异步任务
可以使用group与dispatch_block_cancel将一组编排好的异步任务组中的异步任务进行取消,比方说在某个视图控制器中,发起了N个网络请求,离开界面的时候,如果网络请求没有全部结束,而用户退出该界面,则可以取消这些异步任务回收线程资源,简单实现如下:
//使用Dispatch Group与dispatch_block_cancel,取消异步任务
NSMutableArray * request_blocks = [NSMutableArray array];
dispatch_group_t request_blocks_group = dispatch_group_create();
//开启五个异步网络请求任务
for (int i = 0; i<5; i++) {
dispatch_block_t request_block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
NSURLRequest * request = [[NSURLRequest alloc]init];
dispatch_group_enter(request_blocks_group);
[self postWithRequest:request completion:^(id responseObjecy, NSURLResponse *response, NSError *error) {
//do somethings
dispatch_group_leave(request_blocks_group);
}];
});
[request_blocks addObject:request_block];
dispatch_async(dispatch_get_main_queue(), request_block);
}
//取消这五个任务
for (dispatch_block_t request_block in request_blocks) {
dispatch_block_cancel(request_block);
dispatch_group_leave(request_blocks_group);
}
dispatch_group_notify(request_blocks_group, dispatch_get_main_queue(), ^{
NSLog(@"网络任务请求执行完毕或者全部取消");
});
dispatct_io_t的使用
//dispatch_io_t的使用
NSString * plist_path = [[NSBundle mainBundle]pathForResource:@"Info" ofType:@".plist"];
//获取文件dispatch_fd_t 的描述符
dispatch_fd_t fd = open(plist_path.UTF8String, O_RDONLY);
//创建文件读取所使用的操作队列
dispatch_queue_t queue = dispatch_queue_create("test io", NULL);
//根据描述符创建GCD的读取流
dispatch_io_t pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, queue, ^(int error) {
close(fd);
});
//设置读取大小
dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
//使用文件流来读取内容
dispatch_io_read(pipe_channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t _Nullable data, int error) {
if (error==0) {
}
if (done) {
}
size_t len = dispatch_data_get_size(data);
if (len>0) {
}
});
GCD当中有关io读取的API非常丰富,利用unix风格的io函数来进行文件操作,与队列进行配合,可获取GCD自动调度资源利用多线程进行io操作的能力,同时前面提到的各种异步阻塞或者同步的模型可以利用,创建一套高效的io操作,满足大文件读取的高性能需求。