iOS开发中经常会用到多线程的技术,官方提供的多线程api包括pthread、NSThread、GCD和NSOperation;其中pthread是C的接口,对于开发者来说不够友好,NSThread是一个比较简单的api,使用场景有限,GCD是苹果官方提供的系统级的线程管理,api丰富,执行性能高,NSOperation是基于GCD的封装,本篇文章是工作中对于GCD使用的一些总结。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"do something");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"do some other thing");
});
});
上述代码是GCD最简单的一种用法。子线程进行耗时操作,主线程刷新UI或者做动画,但是GCD的强大远不止这些。dispatch_async和dispatch_sync是block的提交方式,分别代表异步提交和同步提交,队列分为并发队列和串行队列,提交方式和队列的组合有四种场景,如下方表格所示:
并发队列/concurrent | 串行队列/serial | |
---|---|---|
dispatch_async | 并发执行 | 同步执行 |
dispatch_sync | 同步执行效果 | 同步执行效果 |
接下来看一下GCD提供的api:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block):异步提交block进队列的api,提交后会立即返回,不会等待block被执行完,执行队列决定提交进去的block是同步执行还是并发执行。
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block):同步提交block进队列的api,提交后不会立即返回,同时会阻塞当前线程,等到block执行完成。切记不可同步提交block进主队列,结果会造成死锁。
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block):通过该api提交的block,会在该队列完成之前提交的所有的block后才会执行,在这个方法后面的提交的blocks也必须等待当前block执行完才会操作,该api会立即返回,不会阻塞主线程。
dispatch_queue_t aQueue = dispatch_queue_create("com.wanglei.gcddemo.aQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(aQueue, ^{
sleep(2);
NSLog(@"+++++++++++1");
});
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++2");
});
dispatch_barrier_async(aQueue, ^{
sleep(3);
NSLog(@"+++++++++++3");
});
NSLog(@"+++++++++test");
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++4");
});
输出结果如下
2019-04-26 16:58:19.161177+0800 WLTest[15736:1047575] +++++++++test
2019-04-26 16:58:19.161310+0800 WLTest[15736:1047796] +++++++++++2
2019-04-26 16:58:21.166526+0800 WLTest[15736:1049056] +++++++++++1
2019-04-26 16:58:24.167634+0800 WLTest[15736:1049056] +++++++++++3
2019-04-26 16:58:24.167822+0800 WLTest[15736:1049056] +++++++++++4
可见该方法并没有阻塞主线程,任务4也是在barrier任务完成后才执行的。
需要注意的是这里的队列queue不能使用全局队列,必须是自定义的队列。网上有文章指出该api是可以解决多线程并发读写同一个资源时发生死锁的问题,示例代码如下:
@implementation TicketManager
- (instancetype)init {
self = [super init];
if (self) {
self.ticketNumber = 10000;
saleQueue = dispatch_queue_create("com.wanglei.gcddemo.saleQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)sale:(int)number {
dispatch_barrier_async(saleQueue, ^{
if (self.ticketNumber == 0) {
NSLog(@"+++++++++++++++++++++票已售罄");
} else {
if (self.ticketNumber >= number) {
self.ticketNumber -= number;
NSLog(@"+++++++++++++++售出%d张++++++++++++++还剩%d张",number,self.ticketNumber);
} else {
NSLog(@"++++++++++++++++++票数不足,无法完成本次交易");
}
}
});
}
@end
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block):功能与async类似,但是这个会阻塞主线程,不建议使用。
dispatch_queue_t aQueue = dispatch_queue_create("com.wanglei.gcddemo.aQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(aQueue, ^{
sleep(2);
NSLog(@"+++++++++++1");
});
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++2");
});
dispatch_barrier_sync(aQueue, ^{
sleep(3);
NSLog(@"+++++++++++3");
});
NSLog(@"+++++++++test");
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++4");
});
输出结果如下:
2019-04-26 17:05:50.135057+0800 WLTest[15819:1055314] +++++++++++2
2019-04-26 17:05:52.140302+0800 WLTest[15819:1054894] +++++++++++1
2019-04-26 17:05:55.141124+0800 WLTest[15819:1054677] +++++++++++3
2019-04-26 17:05:55.141324+0800 WLTest[15819:1054677] +++++++++test
2019-04-26 17:05:55.141437+0800 WLTest[15819:1054894] +++++++++++4
从输出结果可以看出 dispatch_barrier_sync方法除了会阻塞主线程,其它和dispatch_barrier_async并无二致。一言概括,通过以上两个方法提交的block会保证在队列执行他们的时候队列中其它block不会被执行。苹果的官方注释中说明通过该方法提交的block会被标记为barrier,说明block时可以被标记为barrier的,那么直接创建一个barrier类型的block其实也是可以实现相同的效果。这个在后面的章节会说明。
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,dispatch_block_t block):该方法可以将block在特定时间之后提交进队列中,比较多的应用场景是在主线程中需要延迟执行某段业务逻辑时使用,该方法不会阻塞主线程;而且严格来说该方法的延时效果并不精准,这是传入的时间只是将block提交进队列的时间,但是队列什么时候执行该block则是系统决定的。
dispatch_once:dispatch_once_t要是全局或static变量,保证dispatch_once_t只有一份实例,该方法在应用生命周期内只会执行一次,单例模式下使用该方法。
static TicketManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[TicketManager alloc] init];
});
return manager;
dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t)):该方法是GCD提供的一个遍历方法,类似于我们平时使用的for循环,当队列queue为并发队列时,它的遍历速度是比普通的for循环要快的,需要注意的是该方法会阻塞主线程。
size_t count = 10;
dispatch_apply(count, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
NSLog(@"+++++++++++++%zu",index);
});
NSLog(@"+++++++++test");
输出结果如下:
2019-04-26 23:23:02.315548+0800 WLTest[58050:2296323] +++++++++++++0
2019-04-26 23:23:02.315605+0800 WLTest[58050:2297022] +++++++++++++2
2019-04-26 23:23:02.315601+0800 WLTest[58050:2296754] +++++++++++++1
2019-04-26 23:23:02.315639+0800 WLTest[58050:2297024] +++++++++++++3
2019-04-26 23:23:02.315682+0800 WLTest[58050:2297023] +++++++++++++4
2019-04-26 23:23:02.315760+0800 WLTest[58050:2297026] +++++++++++++6
2019-04-26 23:23:02.315766+0800 WLTest[58050:2296323] +++++++++++++7
2019-04-26 23:23:02.315768+0800 WLTest[58050:2297022] +++++++++++++8
2019-04-26 23:23:02.315769+0800 WLTest[58050:2297025] +++++++++++++5
2019-04-26 23:23:02.315788+0800 WLTest[58050:2296754] +++++++++++++9
2019-04-26 23:23:02.317128+0800 WLTest[58050:2296323] +++++++++test
说完提交方法后,我们再来看看GCD中很重要的概念,队列 。GCD中的队列大概分为三种,主队列、全局队列和自定义队列。
dispatch_get_main_queue():主队列即主线程,该队列为一个串行队列,拥有最高的系统优先级。
dispatch_get_global_queue(long identifier, unsigned long flags):全局队列,identifier为队列优先级,flags是苹果官方的一个保留参数,留作未来使用,当前固定传0,否则有可能返回为空。需要注意的是全局队列都是并发队列。identifier可传参数是一个枚举类型,有以下几种,优先级由高到低排列。
* - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
冒号后面是该优先级对应的qos级别,qos全称为quality of service,苹果官方推荐我们使用qos来与队列优先级一一对应,qos也包含了若干个枚举值,如下:
1. QOS_CLASS_USER_INTERACTIVE 最高的优先级,用来更新UI、响应事件等,官方文档中的说明是该优先级的队列会使用所有的cpu资源和I/O带宽,所以这不是一个高效节能的qos,慎用。
2. QOS_CLASS_USER_INITIATED 仅次于QOS_CLASS_USER_INTERACTIVE的优先级,最好用于由用户操作发起的需要短时间内执行完成的操作
3. QOS_CLASS_DEFAULT 如果用户不指定优先级时的默认优先级 该优先级仅低于1和 2
4. QOS_CLASS_UTILITY 用于执行用户需要但不紧急的任务,例如计算,I/O,网络,持续的数据填充等任务,这是一个高效节能的qos
5. QOS_CLASS_BACKGROUND 最低优先级,用于执行用户不需要也不紧急的任务,这是一个高效节能的qos
对比一下发现,全局队列最多可以获得优先级只是QOS_CLASS_USER_INITIATED,最高的优先级QOS_CLASS_USER_INTERACTIVE对应主队列的优先级。
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr):最后我们再来看一下自定义队列,label参数类似于队列的id,attr参数比较有意思,它标记了所要创建的队列的类型和特征。
attr是一个dispatch_queue_attr_t对象 系统提供了几个预编译好的类型,就是我们常用的DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT
DISPATCH_QUEUE_CONCURRENT:并发配置项,可以同时执行多个block,类似于系统的全局并发队列,但是需要的开销更大,不过它也有它的优势,使用这个配置项生成的自定义队列可以配合dispatch_barrier_async实现闭包执行依赖和解决多线程并发读写同一个资源发生死锁的问题,这个前面已有说明。
DISPATCH_QUEUE_SERIAL:同步配置项,可以生成一个FIFO队列
DISPATCH_QUEUE_CONCURRENT_INACTIVE/DISPATCH_QUEUE_SERIAL_INACTIVE:睡眠配置项,可以生成一个睡眠状态的并发/串行队列,生成的队列在执行block之前必须先通过dispatch_activate唤醒,否则会造成闪退,该队列在唤醒之前可以使用dispatch_set_target_queue方法设置targetqueue,唤醒之后则不可再设置。
DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL/DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL:自动释放配置项,可生成自带@autoreleasepool功能的并发/串行队列,通过dispatch_async(),dispatch_barrier_async(),dispatch_group_notify()方法异步提交的代码块相当于在最外层加上@autoreleasepool。该选项生成的队列是默认唤醒且不可通过dispatch_set_target_queue方法设置targetqueue。
以上系统提供的预编译类型,虽然基本满足了我们的需要,但是有没有发现我们并没有办法像全局队列那样设置队列的优先级?这些预编译的类型的优先级都是QOS_CLASS_DEFAULT,但是我们是可以自己选择的,dispatch_queue_attr_t对象有自己的构造方法。
dispatch_queue_attr_make_with_qos_class(dispatch_queue_attr_t _Nullable attr,dispatch_qos_class_t qos_class, int relative_priority)
attr:队列类型 并发/串行
qos_class:队列优先级
QOS_CLASS_USER_INTERACTIVE <-> 主队列
QOS_CLASS_USER_INITIATED <-> DISPATCH_QUEUE_PRIORITY_HIGH
QOS_CLASS_DEFAULT <-> DISPATCH_QUEUE_PRIORITY_DEFAULT
QOS_CLASS_UTILITY <-> DISPATCH_QUEUE_PRIORITY_LOW
QOS_CLASS_BACKGROUND <-> DISPATCH_QUEUE_PRIORITY_BACKGROUNDrelative_priority:具体用法未知,取值范围是-15~-1。
除此之外还有两个构造方法:
1.dispatch_queue_attr_make_initially_inactive,该方法用于生成自定义的默认睡眠的队列
2.dispatch_queue_attr_make_with_autorelease_frequency,该方法用于生成自定义的带自动释放特性的队列
dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t _Nullable queue):
该方法的作用有:1.变更队列优先级,使object队列的优先级和queue队列的相同,不可指定主队列和全局队列
2.目标队列可以作为原队列的执行队列,可以借此实现多队列同步
示例代码如下:
dispatch_queue_attr_t attr1 = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
dispatch_queue_t attr1Queue = dispatch_queue_create("com.wanglei.gcddemo.attr1Queue", attr1);
dispatch_queue_attr_t attr2 = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, -1);
dispatch_queue_t attr2Queue = dispatch_queue_create("com.wanglei.gcddemo.attr2Queue", attr2);
dispatch_async(attr2Queue, ^{
NSLog(@"attr2");
});
dispatch_async(attr1Queue, ^{
NSLog(@"attr1");
});
dispatch_queue_t serialQueue = dispatch_queue_create("com.wanglei.gcddemo.attr3Queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t attr3Queue = dispatch_queue_create("com.wanglei.gcddemo.attr3Queue", DISPATCH_QUEUE_CONCURRENT_INACTIVE);
dispatch_queue_t attr4Queue = dispatch_queue_create("com.wanglei.gcddemo.attr4Queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(attr3Queue, serialQueue);
dispatch_activate(attr3Queue);
dispatch_set_target_queue(attr4Queue, serialQueue);
dispatch_async(attr3Queue, ^{
sleep(3);
NSLog(@"attr3Queue++++++++++++++1");
});
dispatch_async(attr3Queue, ^{
NSLog(@"attr3Queue++++++++++++++2");
});
dispatch_async(attr4Queue, ^{
NSLog(@"attr4Queue++++++++++++++1");
});
输出结果为:
2019-04-27 00:31:06.875735+0800 WLTest[58296:2357585] attr1
2019-04-27 00:31:06.875804+0800 WLTest[58296:2358101] attr2
2019-04-27 00:31:09.876786+0800 WLTest[58296:2358100] attr3Queue++++++++++++++1
2019-04-27 00:31:09.877133+0800 WLTest[58296:2358100] attr3Queue++++++++++++++2
2019-04-27 00:31:09.877301+0800 WLTest[58296:2358100] attr4Queue++++++++++++++1
需要注意的是默认睡眠的自定义队列在使用之前一定要唤醒,否则会导致闪退;设置目标队列的操作也必须在唤醒前进行。普通自定义队列可以随时调用dispatch_set_target_queue改变目标队列,但是生效时间由系统决定。
最后我们再来看一下GCD的block,毕竟队列都是以block的形式执行任务。
dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block):可用该方法创建一个dispatch_block_t,flags是一个枚举类型,即可以给block做标记,并返回一个做过标记的block,在dispatch_block_flags_t这个枚举类型中第一个类型就是barrier类型:DISPATCH_BLOCK_BARRIER
dispatch_queue_t aQueue = dispatch_queue_create("com.wanglei.gcddemo.aQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t blk = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
sleep(3);
NSLog(@"+++++++++++3");
});
dispatch_async(aQueue, ^{
sleep(2);
NSLog(@"+++++++++++1");
});
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++2");
});
dispatch_async(aQueue, blk);
NSLog(@"+++++++++test");
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++4");
});
输出结果如下:
2019-04-26 17:30:29.744942+0800 WLTest[16034:1072405] +++++++++test
2019-04-26 17:30:29.745070+0800 WLTest[16034:1072526] +++++++++++2
2019-04-26 17:30:31.745508+0800 WLTest[16034:1072465] +++++++++++1
2019-04-26 17:30:34.749852+0800 WLTest[16034:1072465] +++++++++++3
2019-04-26 17:30:34.750104+0800 WLTest[16034:1072465] +++++++++++4
可见结果与使用dispatch_barrier_async是一样的。
dispatch_block_wait(dispatch_block_t block, dispatch_time_t timeout)
该方法会在阻塞主线程,并等待block执行完成或者timeout时间超时才会返回。比如我们在以上示例代码中稍做改动。
dispatch_queue_t aQueue = dispatch_queue_create("com.wanglei.gcddemo.aQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t blk = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
sleep(3);
NSLog(@"+++++++++++3");
});
dispatch_async(aQueue, ^{
sleep(2);
NSLog(@"+++++++++++1");
});
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++2");
});
dispatch_async(aQueue, blk);
dispatch_block_wait(blk, DISPATCH_TIME_FOREVER);
NSLog(@"+++++++++test");
dispatch_async(aQueue, ^{
NSLog(@"+++++++++++4");
});
输出结果如下:
2019-04-27 00:47:39.248981+0800 WLTest[58336:2371407] +++++++++++2
2019-04-27 00:47:41.249047+0800 WLTest[58336:2371406] +++++++++++1
2019-04-27 00:47:44.252109+0800 WLTest[58336:2371406] +++++++++++3
2019-04-27 00:47:44.252463+0800 WLTest[58336:2370834] +++++++++test
2019-04-27 00:47:44.252698+0800 WLTest[58336:2371408] +++++++++++4
可见在主线程打印的test要等到任务3结束后才打印。
dispatch_block_notify(dispatch_block_t block, dispatch_queue_t queue, dispatch_block_t notification_block)
可以监视block是否结束,并在block执行结束后将notification_block加入到指定队列queue中执行。示例代码如下:
dispatch_queue_t queue1 = dispatch_queue_create("com.wanglei.gcddemo.queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("com.wanglei.gcddemo.queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t blk1 = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
sleep(2);
NSLog(@"+++++++++++++blk1");
});
dispatch_async(queue1, blk1);
dispatch_block_t blk2 = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"+++++++++++++blk2");
});
dispatch_block_notify(blk1, queue2, blk2);
输出结果是:
2019-04-27 01:02:31.286807+0800 WLTest[58405:2385058] +++++++++++++blk1
2019-04-27 01:02:31.287180+0800 WLTest[58405:2385058] +++++++++++++blk2
是不是类似于nsoperation中的依赖关系?
dispatch_block_cancel(dispatch_block_t block)
该方法是iOS8开始GCD才支持的方法,它可以取消还未开始执行的block。示例如下:
dispatch_queue_t queue = dispatch_queue_create("com.wanglei.gcddemo.queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t blk = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"+++++++++++++blk");
});
dispatch_async(queue, ^{
sleep(2);
NSLog(@"+++++++++++1");
});
dispatch_async(queue, blk);
dispatch_async(queue, ^{
NSLog(@"+++++++++++2");
});
dispatch_block_cancel(blk);
输出结果如下:
2019-04-27 01:13:07.468454+0800 WLTest[58447:2394080] +++++++++++1
2019-04-27 01:13:07.468768+0800 WLTest[58447:2394080] +++++++++++2
可以看到blk的执行被取消了。
信号量dispatch_semaphore_t简介
信号量是可以跨线程的,它提供了dispatch_semaphore_signal方法来使信号量加1,dispatch_semaphore_wait方法使信号量减1,当信号量小于0时会阻塞当前线程,利用这个特性可以使若干个异步操作变成同步操作,项目中最常见的场景是数据来自多个接口,必须等所有的数据都拿到后才能进行进行相应的页面渲染或者操作,这是一个完美的使用信号量将异步操作转同步操作的场景。我们可以申明一个全局的信号量sema,在子线程使用dispatch_semaphore_wait阻塞,等待接口返回后调用dispatch_semaphore_signal方法使信号量变为0以执行后续操作。注意尽量在子线程阻塞,不要在主线程,示例如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
//do something
});
});
除了信号量,GCD中的group也是可以做到异步变同步的。
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
//do something
dispatch_group_leave(group);
enter和leave方法必须成对出现,就像信号量的wait和singal方法一样。
GCD非常强大,写这篇文章看了很久的官方文档,但依然觉得有很多没弄明白的,但是写文章的过程也让我自己受益匪浅。