GCD (Grand Central Dispatch),纯C语言,提供了很多好用的异步处理函数。
GCD优点:
- 苹果公司为多核的并行运算提出的解决方案
- 会自动的利用更多的CPU内核(双核,四核)来执行任务
- 会自动管理线程的生命周期(创建线程,调度任务,销毁线程)
- 使用简单便捷,只需利用GCD的函数来执行自定义的任务,不需要写线程管理代码
GCD的核心——将任务添加到队列,并且指定执行任务的函数
一、简单地异步任务 dispatch_block_t
dispatch_block_t block = ^{
// 任务 打印字符串
NSLog(@"GCD 的一个简单block,无参 无返回值的");
};
dispatch_queue_t queue = dispatch_queue_create("com.film.test", NULL);
dispatch_async(queue, block);
此代码最能体现GCD代码使用核心:
-
dispatch_block_t
使用block封装任务 -
dispatch_queue_t
创建队列 -
dispatch_async
将任务添加到队列
通常简写如下:
dispatch_queue_t queue = dispatch_queue_create("com.film.test", NULL);
dispatch_async(queue, ^{
// 任务 打印字符串
NSLog(@"GCD 的一个简单block,无参 无返回值的");
});
二、同步&异步—— dispatch_sync & dispatch_async
1:dispatch_sync
同步执行
- 必须等待当前语句执行完毕,才会执行下一条语句
- 不会开启线程
- 在当前线程执行block的任务
2:dispatch_async
异步执行
- 不用等待当前语句执行完毕,就可以执行下一条语句
- 会开启线程执行block任务
- 异步是多线程的代名词
三:串行队列&并发队列—— dispatch_queue_t
多线程中队列分为
串行队列(Serial Dispatch Queue)
和并发队列(Concurrent Dispatch Queue)
:
串行队列
:线程执行只能依次逐一先后有序的执行,等待上一个执行完再执行下一个。使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL)
创建串行队列,或者使用dispatch_queue_create("xxx", NULL)
并发队列
:线程可以同时一起执行,不需要等待上一个执行完就能执行下一个任务。使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
创建并发队列主队列
:绑定主线程,所有任务都在主线程中执行、经过特殊处理的串行的队列, 使用dispatch_get_main_queue()
获取主队列全局队列
:系统提供的并发队列, 最简单的是使用dispatch_get_global_queue(0, 0)
获取系统提供的并发队列, 第一个参数是优先级枚举值,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0
, 优先级从高到低依次为DISPATCH_QUEUE_PRIORITY_HIGH
、DISPATCH_QUEUE_PRIORITY_DEFAULT
、DISPATCH_QUEUE_PRIORITY_LOW
、DISPATCH_QUEUE_PRIORITY_BACKGROUND
四、串行/并发和同步/异步的排列组合
1:串行+同步
任务一个接一个,不开辟线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"串行&同步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 串行&同步线程0-{number = 1, name = main}
// 串行&同步线程1-{number = 1, name = main}
// ...按顺序输出
--------------------输出结果:-------------------
2:串行+异步
任务一个接一个,会开辟新线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"串行&异步线程%d- %@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 串行&异步线程 0-{number = 6, name = (null)}
// 串行&异步线程 1-{number = 6, name = (null)}
// ... 按顺序输出
--------------------输出结果:-------------------
3:并发+同步
任务一个接一个,不开辟线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"并发&同步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 串行&同步线程0-{number = 1, name = main}
// 串行&同步线程1-{number = 1, name = main}
// ...按顺序输出
--------------------输出结果:-------------------
4:并发+异步
任务乱序,开辟新线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"并发&异步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 并发&异步线程1-{number = 5, name = (null)}
// 并发&异步线程0-{number = 4, name = (null)}
// ...乱序输出
--------------------输出结果:-------------------
5:主队列+同步
互相等待,造成死锁
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"主队列&同步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// crash...
--------------------输出结果:-------------------
6:主队列+异步
任务一个接一个执行,不开辟线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"主队列&异步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 主队列&异步线程0-{number = 1, name = main}
// 主队列&异步线程1-{number = 1, name = main}
// ...按顺序输出
--------------------输出结果:-------------------
7:全局队列+同步
任务一个接一个执行,不开辟线程
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"全局队列&同步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 全局队列&同步线程0-{number = 1, name = main}
// 全局队列&同步线程1-{number = 1, name = main}
// ...按顺序输出
--------------------输出结果:-------------------
8:全局队列+异步
任务乱序执行,开辟线程(同 并发+异步)
- (void)test {
NSLog(@"主线程-%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"全局队列&异步线程%d-%@", i, [NSThread currentThread]);
});
}
}
--------------------输出结果:-------------------
// 主线程-{number = 1, name = main}
// 全局队列&异步线程2-{number = 3, name = (null)}
// 全局队列&异步线程3-{number = 7, name = (null)}
// ...乱序输出
--------------------输出结果:-------------------
总结
执行/队列 | 串行队列 | 并发队列 | 主队列 | 全局队列 |
---|---|---|---|---|
同步执行 | 按序执行,不开辟线程 | 安序,不开 | 死锁,crash | 安序,不开 |
异步执行 | 安序,开辟 | 乱序,开辟 | 安序,不开 | 乱序,不开 |
五、延迟执行 dispatch_after
dispatch_after
表示某队列中的block任务延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2秒后输出");
});
六、dispatch_once
dispatch_once
保证App运行期间,block中的代码只执行一次
应用场景:单例、method-swizzling
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//创建单例、method swizzled或其他任务
});
七、dispatch_apply
dispatch_apply
将指定的Block追加到指定的队列中重复执行,并等到全部的处理执行结束——相当于线程安全的for循环
应用场景:用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性
1:添加到串行队列中——按序执行
2:添加到主队列中——死锁
3:添加到并发队列中——乱序执行
4:添加到全局队列中——乱序执行
- (void)test {
/**
param1:重复次数
param2:追加的队列
param3:执行任务
*/
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
NSLog(@"dispatch_apply前");
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"dispatch_apply的线程%zu-%@", index, [NSThread currentThread]);
});
NSLog(@"dispatch_apply后");
}
--------------------输出结果:-------------------
// dispatch_apply前
// dispatch_apply的线程0-{number = 1, name = main}
// ... 是否按序输出与 串行队列还是并发队列有关
// dispatch_apply后
--------------------输出结果:-------------------
八、dispatch_group_t
dispatch_group_t
:调度组将任务分组执行,能监听任务组完成,并设置等待时间
应用场景:多个接口请求之后刷新页面
1.dispatch_group_async
dispatch_group_notify
在dispatch_group_async
执行结束之后会受到通知
- (void)test {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"请求一完成");
});
dispatch_group_async(group, queue, ^{
NSLog(@"请求二完成");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
}
--------------------输出结果:-------------------
// 请求二完成
// 请求一完成
// 刷新页面
--------------------输出结果:-------------------
2:dispatch_group_enter & dispatch_group_leave
dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰
- (void)test {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求二完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
}
--------------------输出结果:-------------------
// 请求二完成
// 请求一完成
// 刷新页面
--------------------输出结果:-------------------
调度组要注意搭配使用,必须新进组再出组,缺一不可。
3:dispatch_group_wait使用
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)
group
:需要等待的调度组,
timeout
:等待的超时时间(即等多久),设置为DISPATCH_TIME_NOW
意味着不等待直接判定调度组是否执行完毕
,设置为DISPATCH_TIME_FOREVER
则会阻塞当前调度组,直到调度组执行完毕
返回值:为long
类型,返回值为0——在指定时间内调度组完成了任务,返回值不为0——在指定时间内调度组没有按时完成任务
- (void)test {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求二完成");
dispatch_group_leave(group);
});
long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);
// long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
NSLog(@"timeout=%ld", timeout);
if (timeout == 0) {
NSLog(@"按时完成任务");
} else {
NSLog(@"超时");
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
}
--------------------输出结果:-------------------
// timeout=49
// 请求一完成
// 请求二完成
// 超时
// 刷新页面
--------------------输出结果:-------------------
九、dispatch_barrier_sync & dispatch_barrier_async
应用场景:同步锁
前文已经提过并发执行异步队列会开辟线程,而任务也会因为任务复杂度和cpu的调度导致各个乱序执行完毕,比如上图中的任务3明明是先于任务4执行,但是晚于任务4执行完毕
此时GCD就提供了两个API——
dispatch_barrier_sync
和dispatch_barrier_async
,使用这两个API就能将多个任务进行分组——等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中。简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务
1:串行队列使用栅栏函数
- (void)test {
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
NSLog(@"开始——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s的任务1——%@", [NSThread currentThread]);
});
NSLog(@"第一次结束——%@", [NSThread currentThread]);
dispatch_barrier_async(queue, ^{
NSLog(@"----------栅栏任务----------%@", [NSThread currentThread]);
});
NSLog(@"栅栏结束——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"延迟1s的任务2——%@", [NSThread currentThread]);
});
NSLog(@"第二次结束——%@", [NSThread currentThread]);
}
开始——{number = 1, name = main}
第一次结束——{number = 1, name = main}
栅栏结束——{number = 1, name = main}
第二次结束——{number = 1, name = main}
延迟2s的任务1——{number = 5, name = (null)}
----------栅栏任务----------{number = 1, name = main}
延迟1s的任务2——{number = 5, name = (null)}
注释掉栅栏函数的话,得到:
开始——{number = 1, name = main}
第一次结束——{number = 1, name = main}
第二次结束——{number = 1, name = main}
延迟2s的任务1——{number = 3, name = (null)}
延迟1s的任务2——{number = 3, name = (null)}
栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
结论:由于
串行队列异步
执行任务是一个接一个的执行的,所以使用栅栏函数无意义
**2:并发队列使用栅栏函数
- (void)test {
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s的任务1——%@", [NSThread currentThread]);
});
NSLog(@"第一次结束——%@", [NSThread currentThread]);
// dispatch_barrier_async(queue, ^{
// NSLog(@"----------栅栏任务----------%@", [NSThread currentThread]);
// });
// NSLog(@"栅栏结束——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"延迟1s的任务2——%@", [NSThread currentThread]);
});
NSLog(@"第二次结束——%@", [NSThread currentThread]);
}
开始——{number = 1, name = main}
第一次结束——{number = 1, name = main}
第二次结束——{number = 1, name = main}
延迟1s的任务2——{number = 5, name = (null)}
延迟2s的任务1——{number = 7, name = (null)}
打开注释,使用栅栏函数:
开始——{number = 1, name = main}
第一次结束——{number = 1, name = main}
栅栏结束——{number = 1, name = main}
第二次结束——{number = 1, name = main}
延迟2s的任务1——{number = 4, name = (null)}
----------栅栏任务----------{number = 4, name = (null)}
延迟1s的任务2——{number = 4, name = (null)}
** 并发队列异步执行
任务是乱序的,所以使用栅栏函数可以很好地控制队列内任务的执行顺序**
假如将前面测试案例2中的 dispatch_barrier_async
改成 dispatch_barrier_sync
,结果发生了阻塞。
结论:dispatch_barrier_async
可以控制队列中任务的执行顺序,而dispatch_barrier_sync
不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)
4.栅栏函数注意点
1:尽量使用自定义的并发队列
使用全局队列起不到栅栏函数的作用
使用全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
2:栅栏函数只能控制同一并发队列
打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列
十、dispatch_semaphore_t
应用场景:同步当锁, 控制GCD最大并发数
-
dispatch_semaphore_create()
:创建信号量 -
dispatch_semaphore_wait()
:等待信号量,信号量减1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)才执行下去 -
dispatch_semaphore_signal()
:释放信号量,信号量加1。当信号量>= 0 会执行wait之后的代码
下面这段代码要求使用信号量来按序输出(当然栅栏函数可以满足要求)
- (void)test {
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前%d----线程%@", i, [NSThread currentThread]);
});
// 使用栅栏函数
// dispatch_barrier_async(queue, ^{});
}
}
使用信号量API
- (void)test {
// 创建信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前%d----线程%@", i, [NSThread currentThread]);
// 打印任务结束后信号量解锁
dispatch_semaphore_signal(sem);
});
// 由于异步执行,打印任务会较慢,所以这里信号量加锁
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
}
当前0----线程{number = 5, name = (null)}
当前1----线程{number = 5, name = (null)}
当前2----线程{number = 5, name = (null)}
当前3----线程{number = 5, name = (null)}
当前4----线程{number = 5, name = (null)}
当前5----线程{number = 5, name = (null)}
当前6----线程{number = 5, name = (null)}
当前7----线程{number = 5, name = (null)}
当前8----线程{number = 5, name = (null)}
当前9----线程{number = 5, name = (null)}
如果当创建信号量时传入值为1又会怎么样呢?
- i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1
- i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去
当前1----线程{number = 3, name = (null)}
当前0----线程{number = 6, name = (null)}
当前2----线程{number = 3, name = (null)}
当前3----线程{number = 6, name = (null)}
当前4----线程{number = 6, name = (null)}
当前5----线程{number = 3, name = (null)}
当前6----线程{number = 3, name = (null)}
当前7----线程{number = 6, name = (null)}
当前8----线程{number = 3, name = (null)}
当前9----线程{number = 6, name = (null)}
结论:
创建信号量时传入值为1时,可以通过两次才堵塞
传入值为2时,可以通过三次才堵塞
十一、dispatch_source
应用场景:GCDTimer
1.定义及使用
dispatch_source
是一种基本的数据类型,可以用来监听一些底层的系统事件
Timer Dispatch Source
:定时器事件源,用来生成周期性的通知或回调
Signal Dispatch Source
:监听信号事件源,当有UNIX信号发生时会通知
Descriptor Dispatch Source
:监听文件或socket事件源,当文件或socket数据发生变化时会通知
Process Dispatch Source
:监听进程事件源,与进程相关的事件通知
Mach port Dispatch Source
:监听Mach端口事件源
Custom Dispatch Source
:监听自定义事件源
主要使用的API:
dispatch_source_create
: 创建事件源
dispatch_source_set_event_handler
: 设置数据源回调
dispatch_source_merge_data
: 设置事件源数据
dispatch_source_get_data
: 获取事件源数据
dispatch_resume
: 继续
dispatch_suspend
: 挂起
dispatch_cancel
: 取消
2.自定义定时器
在iOS开发中,一般使用NSTimer
来处理定时逻辑,但NSTimer
是依赖RunLoop
的,而RunLoop
可以运行在不同的模式下。如果NSTimer 添加在一种模式下,当RunLoop切换运行模式的时候,定时器就失去作用了。若RunLoop在阻塞状态,NSTimer触发时间就会推迟到下一个RunLoop周期,因此NSTimer在计时上会有误差,而GCD定时器不依赖RunLoop,计时更加精确。
@property (nonatomic, strong) dispatch_source_t timer;
//1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.创建timer
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//3.设置timer首次执行时间,间隔,精确度
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
//4.设置timer事件回调
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"GCDTimer");
});
//5.默认是挂起状态,需要手动激活
dispatch_resume(_timer);
使用dispatch_source
自定义定时器注意点:
-
GCDTimer
需要强持有,否则出了作用域立即释放,也就没有了事件回调 -
GCDTimer
默认是挂起状态,需要手动激活 -
GCDTimer
没有repeat,需要封装来增加标志位控制 -
GCDTimer
如果存在循环引用,使用weak+strong
或者提前调用dispatch_source_cancel
取消timer -
dispatch_resume
和dispatch_suspend
调用次数需要平衡
source在挂起状态下,如果直接设置source = nil或者重新创建source都会造成crash
。正确的方式是在激活状态下调用dispatch_source_cancel(source)
释放当前的source
GCD常用API,大体包括这些,还有待探索其他API的使用情况。