任务提交方式与队列的总结
Grand Central Dispatch(GCD)是苹果推荐使用的多线程并行执行任务的方案,在多核硬件在 iOS 和 OS X 均支持。
1. 关于GCD中队列的概念
Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in iOS and OS X.
GCD provides and manages FIFO queues to which your application can submit tasks in the form of block objects. Blocks submitted to dispatch queues are executed on a pool of threads fully managed by the system. No guarantee is made as to the thread on which a task executes. GCD offers three kinds of queues:
- Main: tasks execute serially on your application’s main thread.
- Serial: tasks execute one at a time in FIFO order.
- Concurrent: tasks are dequeued in FIFO order, but run concurrently and can finish in any order.
The main queue is automatically created by the system and associated with your application’s main thread. Your application uses one (and only one) of the following three approaches to invoke blocks submitted to the main queue:
- Calling
dispatch_main
- Using a
CFRunLoopRef
on the main thread- Calling
UIApplicationMain
(iOS) orNSApplicationMain
(OS X)
GCD会自动在app中创建三个全局并行队列,这三个队列的优先级不同,并且由系统来维护,不需要开发者手动保持和释放。我们可以用dispatch_get_global_queue
来获取相应的全局队列,当然也可以手动创建并行队列为自己所用。
在串行队列中任务依次执行,串行队列可以被创建多个,在使用过程中避免与并行队列混淆以及死锁的产生。如,在同一个线程且同一个串行队列中同步提交一个任务,就会产生死锁。
2. 在GCD中线程与队列的关系
Serial: tasks execute one at a time in FIFO order.
Concurrent: tasks are dequeued in FIFO order, but run concurrently and can finish in any order.
- 串行,只会开启一条线程
任务以FIFO从序列中一个一个执行。一次只调度一个任务,队列中的任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)。 - 并发,会自动开启多个线程同时执行任务
任务以FIFO从序列中移除,然后并发运行,可以按照任何顺序完成。
//// 异步任务 + 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.dcxz.crop", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i< 10;i++){
// 提交10个异步任务
dispatch_async(queue, ^{
NSLog(@"%@--%d",[NSThread currentThread], i);
});
}
// thread的地址一样,且顺序输出0、1、2 ...
// 也即,“异步 + 串行”只会创建一个新线程,且依次串行执行任务
//// 同步任务 + 并行队列
dispatch_queue_t queue = dispatch_queue_create("com.lai.www", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i< 10;i++){
// 提交10个同步
dispatch_sync(queue, ^{
NSLog(@"%@--%d",[NSThread currentThread], i);
});
}
// thread为主线程(main),且顺序输出0、1、2 ...
// 也即,同步不会创建新线程,且所有的任务依次在当前线程上执行
再来审视一下,任务提交方式与不同队列的组合:
调度方式 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
dispatch_async | 开启多个新线程,并发执行任务 | 开启1个新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
dispatch_sync | 不开启新线程,串行执行任务 | 不开启新线程,串行执行任务 | 主线程调用:死锁导致程序卡死 |
总结:
- 是否开启新线程取决于提交任务的方式;而任务间的依赖关系则取决于所在队列的类型;
- 全局队列,尤其是主队列稍显特殊;同步提交任务需要避免死锁,上面有描述。
3. 多线程中队列的选型
- dispatch_group
等待group中多个异步任务执行完毕,做一些事情。其中,一般提交任务使用并行队列,自建或全局都可以,而notify时,可以转为其他队列。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 异步任务1
});
dispatch_group_async(group, queue, ^{
// 异步任务2
});
// 方式1(不推荐,会卡住当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//...
// 方式2(推荐)
dispatch_group_notify(group, mainQueue, ^{
// 任务完成后,在主队列中做一些操作
//...
});
另一种dispatch_group的使用方式为:
dispatch_group_enter(group);
dispatch_async(queue, ^{ // 异步线程的耗时操作
// 数据返回后一些处理
// ...
dispatch_group_leave(group);
});
// 以上组合,与dispatch_group_async异曲同工
// 真实场景中,异步线程的耗时操作,可能是一些带回调的复杂方法
dispatch_group_notify(group, mainQueue, ^{
// 一般为回主队列刷新UI
//...
});
- dispatch_barrier
dispatch_barrier_async(sync)的作用是保证在其前提交的任务均先于自己执行,在其后提交的任务均迟于自己执行。
// 创建一个并行队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 任务1
// ...
});
dispatch_async(queue, ^{
// 任务2
// ...
});
dispatch_barrier_async(queue, ^{ // 承上启下的 barrier
// 任务3
// ...
});
dispatch_async(queue, ^{
// 任务4
// ...
});
dispatch_async(queue, ^{
// 任务5
// ...
});
与dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,实现读写操锁经常需要用到dispatch_barrier。dispatch_barrier_async(sync)只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
- dispatch_apply
在for循环中处理很多任务时,任务的依次执行,所有任务的耗时也逐次累加。在任务间有递进的依赖关系时可以理解,但在任务间没有依赖关系的场景下体验非常不好。
当且仅当处理顺序/输出顺序对处理结果无影响时,建议采用dispatch_apply代替for:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// dispatch_apply函数是dispatch_sync函数和dispatch_group的关联API
// dispatch_apply会阻塞当前线程,因此,done会最后输出
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
NSLog(@"done");
由于dispatch_apply的并行机制,其效率一般快于for的串行机制(在for一次循环中的处理任务很多时差距比较大),应用场景可以自行畅想一下😄。
- dispatch_suspend / dispatch_resume
顾名思义,这两个方法不会影响到队列中已经开始执行的任务,队列暂停后,已经添加到队列中但还没有执行的任务不会被执行,直到队列被恢复。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_suspend(queue); //暂停队列queue
dispatch_resume(queue); //恢复队列queue
4. GCD中的任务
注意:关于任务的暂定、恢复、取消,分别从队列、任务两个方面表述???
在GCD中向队列中添加的任务是一个 block,任务是否执行完成、是否被取消等状态是可以被监听到的,这个过程需要记录这个 block。
- 创建一个任务(block)
//// 方式一
// flags 为枚举类型,block任务的标记
// block参数是具体的任务
dispatch_block_t dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block);
//// 方式二
// qos_class 为枚举类型,用于设置任务的优先级
dispatch_block_t dispatch_block_create_with_qos_class(dispatch_block_flags_t flags, dispatch_qos_class_t qos_class, int relative_priority, dispatch_block_t block);
- 监听任务状态(block)
等待特定的 block
执行完成之后,再去执行其他任务:
//// 方式一
// 如果任务所需的时间小于 timeout,则返回 0,否则返回非 0 值
// dispatch_block_wait 会阻塞当前线程,避免放在主线程中直接调用
long dispatch_block_wait(dispatch_block_t block, dispatch_time_t timeout);
//// 方式二
// 参数一是需要监视的block,参数二是监听的block执行结束之后要提交执行的队列 `queue`,参数三是待加入到队列中的 `block`
// dispatch_block_notify 不会阻塞当前线程
void dispatch_block_notify(dispatch_block_t block, dispatch_queue_t queue, dispatch_block_t notification_block);
//// dispatch_block_wait 示例 -----------------------------------------------------
dispatch_queue_t concurrentQuene = dispatch_queue_create("concurrentQuene", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQuene, ^{
dispatch_queue_t allTasksQueue = dispatch_queue_create("allTasksQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t block = dispatch_block_create(0, ^{
NSLog(@"开始执行");
[NSThread sleepForTimeInterval:3];
NSLog(@"结束执行");
});
dispatch_async(allTasksQueue, block); // 等待时长,10s 之后超时
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC));
long resutl = dispatch_block_wait(block, timeout);
if (resutl == 0) {
NSLog(@"执行成功");
}
else {
NSLog(@"执行超时");
}
});
//// dispatch_block_notify 示例 -----------------------------------------------------
NSLog(@"---- 开始设置任务 ----");
dispatch_queue_t serialQueue = dispatch_queue_create("com.fyf.serialqueue", DISPATCH_QUEUE_SERIAL);
// 耗时任务
dispatch_block_t taskBlock = dispatch_block_create(0, ^{
NSLog(@"开始耗时任务");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"完成耗时任务");
});
dispatch_async(serialQueue, taskBlock);
// 更新 UI
dispatch_block_t refreshUI = dispatch_block_create(0, ^{
NSLog(@"更新 UI");
});
// 设置监听
dispatch_block_notify(taskBlock, dispatch_get_main_queue(), refreshUI);
NSLog(@"---- 完成设置任务 ----");
- 取消特定任务(block)
主要是使用 dispatch_block_cancel 方法:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block1 = dispatch_block_create(0, ^{
NSLog(@"block1 begin");
[NSThread sleepForTimeInterval:3];
NSLog(@"block1 end");
});
dispatch_block_t block2 = dispatch_block_create(0, ^{
NSLog(@"block2 ");
});
dispatch_async(queue, block1);
dispatch_async(queue, block2);
//取消执行block2
dispatch_block_cancel(block2);
5. 队列的挂起和恢复
挂起指定队列:dispatch_suspend(queue);
恢复指定队列:dispatchp_resume(queue);
// 一个串行队列
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 任务1
sleep(5);
NSLog(@"After 5 seconds...");
});
dispatch_async(queue, ^{ // 任务2
sleep(5);
NSLog(@"After 5 seconds again...");
});
sleep(1);
dispatch_suspend(queue);// 任务1 继续执行,而任务2在队列中被挂起
sleep(10);
dispatch_resume(queue);// 任务2在队列中,恢复执行
dispatch_suspend 不会暂停正在运行任务,当前任务会继续执行,同时暂停后续的block执行。