GCD(grand central dispatch)概要
什么是GCD
是异步执行任务的技术之一。它将应用程序中线程管理的代码放在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,至于具体是哪个线程执行、如何执行该任务,开发者不需管,交由GCD来管理。
因此GCD优于其他异步技术的点:线程管理交由系统管理,效率更高。
线程
源码经过编译链接后生成二进制目标代码(CPU命令列),CPU顺序执行这些命令列。 一个CPU核执行CPU命令列是一条无分叉的路径,即为线程。当一个线程被挂起时,当前CPU的寄存器等信息会保存到各自路径专用的内存块中(保存上下文),线程恢复继续执行,则从内存块中取出数据,复原CPU寄存器等信息,继续执行切换路径的CPU命令列(上下文切换)。
多线程编程容易发生的问题:
多线程的优点:
GCD的API
总览图:
Dispatch Queue
Dispatch Queue:任务队列,存储等待执行的任务。它会按照追加顺序(FIFO)执行任务
队列种类
- 串行队列(Serial Dispatch Queue):其他任务要等待现在执行中的任务处理结束(一个队列对应1个线程(线程可变),即同时只能执行一个任务),有序。
- 并行队列(Concurrent Dispatch Queue):其他任务不需等待现在执行中的任务处理结束(一个队列对应>=1个线程,具体多少要看当前的系统资源,即同时可执行多个任务),无序。
串行队列缺点:每个串行队列对应一个线程,当同时创建多个串行队列时,对应多个线程。此时可能会发生数据竞争。 而一个并行队列虽然同时会有多个线程,但是不管生成多少线程,都会有XNU内核来管理,所以不需担心串行队列的问题。
获取队列的方式
- 自己创建:dispatch_queue_create
// DISPATCH_QUEUE_SERIAL/NULL/0 串行队列
// DISPATCH_QUEUE_CONCURRENT 并行队列
// ARC情况不需要手动释放
dispatch_queue_t queue = dispatch_queue_create("queueIdentifier", 0);
dispatch_async(queue, ^{
NSLog(@"执行任务");
});
复制代码
- 获取系统提供的队列
主队列:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
复制代码
主队列是在main()被调用前,自动被系统创建的串行队列,和主线程相关联。(但这并不代表主队列中的任务只能被主线程执行)。
如果想要(其他线程)执行主队列中的任务,只能用以下三种方式中的一种:
- 调用dispatch_main:叫停主线程,等待主队列中的任务执行(其他线程),永远不会返回
- 调用UIApplicationMain (iOS) or NSApplicationMain (macOS):创建应用、应用代理、和事件循环,永远不会返回
- 在主线程上使用一个CFRunLoopRef
全局并行队列:Global Dispatch Queue
/*
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
*/
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
复制代码
注意:使用函数自己创建的队列的优先级和globalQueue中默认优先级是同等水平的。如果想要改变队列的优先级需要使用函数dispatch_set_target_queue。
变更手动创建的队列的优先级
dispatch_set_target_queue(要改变的对象obj, 目标队列tq)
复制代码
给一个队列设置目标队列会改变这个队列的某些行为:
- 分发队列
- 队列本身未设置优先级,则它的优先级会继承目标队列的优先级;
- 目标队列决定了obj的销毁函数在哪个队列上被执行;
- 通常不会对已经加入的任务造成影响;
- obj会持有tq;
- obj如果是globalQ、mainQ,无效;
- 分发资源
- 分发I/O通道
注意:设置目标队列是,不要形成继承循环。
- 目标队列为串行队列:
dispatch_queue_t serialQ = dispatch_queue_create("serialQ", NULL);
dispatch_queue_t sQ2 = dispatch_queue_create("serialQ2", NULL);
dispatch_queue_t sQ3 = dispatch_queue_create("serialQ3", NULL);
dispatch_async(serialQ, ^{
NSLog(@"serialQ_1---------->");
});
dispatch_async(sQ3, ^{
NSLog(@"serialQ_3----------->");
});
dispatch_async(sQ2, ^{
NSLog(@"serialQ_2----------->");
});
此时结果是不定的,因为三个serialQueue是并行的。
复制代码
dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t serialQ = dispatch_queue_create("serialQ", NULL);
dispatch_queue_t sQ2 = dispatch_queue_create("serialQ2", NULL);
dispatch_queue_t sQ3 = dispatch_queue_create("serialQ3", NULL);
dispatch_set_target_queue(serialQ, globalQ);
dispatch_set_target_queue(sQ2, serialQ);
dispatch_set_target_queue(sQ3, serialQ);
dispatch_async(serialQ, ^{
NSLog(@"serialQ_1---------->");
});
dispatch_async(sQ3, ^{
NSLog(@"serialQ_3----------->");
});
dispatch_async(sQ2, ^{
NSLog(@"serialQ_2----------->");
});
此时结果是确定的 1、3、2。并且使用的是同一个线程
复制代码
正常情况下:无论是串行还是并发队列,只要设置了目标队列,之后任务都会向上传到它的上一级目标队列中。因此任务其实最后都传到根上去了。所以现在这条链上的队列应该都是公用一个线程池的。因此如果要获取当前队列,即获取执行当前代码所关联的队列,结果是不定的。
- 目标队列是并行队列:
定时追加任务--dispatch_after()
设置一定时间后,将任务异步添加到队列中(而不是将任务加到队列中,一定时间后执行),因此时间可能不够准确。
函数直接返回。
- 相对时间
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"执行任务");
});
复制代码
- 绝对时间 设置在某一具体时间(2011/3/25-11:00:03)时,将任务异步添加到队列中。
- (dispatch_time_t)getDispatchTimeByDate:(NSDate *)date {
NSTimeInterval interval;
double second, subsecond;
struct timespec time;
dispatch_time_t milestone;
interval = [date timeIntervalSince1970];
subsecond = modf(interval, &second); // 取浮点数的小数部分和整数部分
time.tv_sec = second;
time.tv_nsec = subsecond * NSEC_PER_SEC; // 小数部分转纳秒
milestone = dispatch_walltime(&time, 0);
return milestone;
}
复制代码
Dispatch Group
当希望知道加入到队列中的任务何时全部执行完成时,有两种方式:1. 将任务加入到串行队列,结束的任务加到最后即可。2. 任务加到多个队列中或者加到并行队列中,需要用到group。
- notify
代码解析:
// 将任务block异步加到队列中,并且将block关联到group群组中,即block持有group
dispatch_group_async(group, globalQ, ^{
NSLog(@"global_blk0---->%@",[NSThread currentThread]);
});
// 当持有group的所有任务都执行完成后,会将任务加入到队列中去执行。如果group中无任务,则立即将任务加到指定队列中
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done---->%@",[NSThread currentThread]);
});
复制代码
- wait
dispatch_barrier_async
访问数据时,容易出现数据竞争的问题。 写操作不允许其他读写操作并行执行,读操作可与其他读操作并行执行。
需要dispatch_barrier_async为需要单独执行的操作加入栅栏。
代码:
同步执行--dispatch_sync()
类似于前面的dispatch_wait,线程执行到该函数,会进入等待状态,直到同步的任务结束,才会返回,继续执行。
容易造成的问题:死锁
// 线程进入等待状态,直到barrierBlock执行完成才会返回
dispatch_barrier_sync(serialQ, ^{
});
复制代码
dispatch_apply
dispatch_apply函数结合了dispatch_sync和dispatch group的功能。该函数按照指定次数将任务block添加到队列中,并等待全部任务执行完成,再返回。
并发队列:
应用:(不关心数据顺序时)
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(global, ^{
dispatch_apply([array count], global, ^(size_t i) {
NSLog(@"%@", array[i]);
});
});
复制代码
暂停和恢复队列中的任务
- dispatch_suspend(dispatch_object_t): 挂起指定队列,已经执行的任务不受影响,尚未执行的停止执行;
- dispatch_resume(dispatch_object_t): 恢复指定队列,继续执行队列中的任务。
注意:
① 1,2必须是一对一的,因为执行1,dispatch_object_t的suspension count会+1;执行2会减一。当count>0时,会一直处于挂起状态。
② 当要暂停一个队列时,上述函数仅对自己手动创建的队列有效,对于系统队列(global、main)无效的。
- iOS 8之后,dispatch_block_cancel(dispatch_block_t)可以取消任务,但是仅能取消等待执行的任务,不能取消正在执行的任务。
信号量-- dispatch semaphore
思想类似停车场:初始有n个车位,每来一辆车,先查看n是否大于0,如果n>0,车辆进入使用车位,n--。每辆车离开停车场n++。 当初始值设置为1时,可以作为锁来使用。
保证任务只执行一次--dispatch_once
dispatch_once函数保证在整个应用程序执行中,只执行一次指定任务,即使在多线程环境下。
应用:单例模式
static NSObject *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[NSObject alloc] init];
});
return instance;
复制代码
Dispatch I/O
读取较大文件时,可将其分割成合适大小并发读取,提高读取速度。