GCD学习
前言
前期对于GCD一直没有一个系统的学习,仅仅是使用了GCD进行网络申请时解决异步问题,本篇文章来总结一下GCD,学习总结一下没有学到的地方。
GCD简介
GCD
全称Grand Central Dispatch
,它是全C代码,其提供了非常多强大的函数。
GCD
的优势:
- 其为苹果公司为多核的并行运算提出的解决方案。
- 其会自动利用更多的CPU内核。
- 其会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 我们在使用
GCD
的时候,只需告诉他我们想要执行什么任务,不需要编写任务线程管理代码
概括一下GCD就是:将任务添加到队列,并且制定执行任务的函数
函数
在GCD中执行任务的方式有两种,同步执行和异步执行;他们分别对应同步函数dispatch_sync
与异步函数dispatch_async
,下面看看两者对比:
- 同步执行,对应同步函数
dispatch_sync
- 必须等待当前语句执行完成之后,才能执行下一条语句
- 不会开启线程,不具备开启线程的能力
- 在当前线程中执行block任务
- 异步执行,对应异步函数
dispatch_async
- 不用等待当前语句执行完成,就可以执行下一条语句
- 会开启线程执行block任务,具备开启新线程的能力
- 异步即为多线程的代名词
综上所述,我们来总结一下两种执行方式的主要区别:
- 是否等待队列的任务执行完毕
- 是否具备开启新线程的能力
任务与队列
下面来看看GCD的两个核心:任务与队列:
- 任务:需要执行什么操作
- 队列:用于存放任务
使用步骤:
创建任务:确定要做的事情
将任务添加到队列中去
- GCD会自动将队列中的任务取出来,而后放到对应的线程中去执行。
- 任务的取出遵循
FIFO
原则:先进先出,后进后出
队列分为串行队列和并发队列,这里给一张图先来看看:
- 串行队列:每次只有一个任务被执行,等上一个结束了再执行下一个,即只开启了一个线程。
- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);
创建串行队列 - 其中
DISPATCH_QUEUE_SERIAL
可以用NULL表示,这两种表达方式都表示默认的串行队列。
- 使用
- 并发队列:一次可以并发执行多个任务,即开启多个线程,并且同时执行任务。
- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
创建并发队列 - 注意:并发队列的并发功能只有在异步函数下才有效。
- 使用
主队列和全局并发队列
在GCD中,针对以上两种队列,分别提供了主队列和全局并发队列:
主队列(Main Dispatch Queue):GCD中提供的特殊的串行队列
- 专门用来在主线程上调度任务的串行队列,依赖于主线程、主
Runloop
,在main函数调用之前自动创建。 - 不会开启线程
- 如果当前主线程正在有任务执行,那么无论主队列中被添加什么任务,都不会被调度。
- 使用
dispatch_get_main_queue()
获得主队列 - 通常在返回主线程更新UI的时候使用
dispatch_queue_t serialQueue = dispatch_get_main_queue();
- 专门用来在主线程上调度任务的串行队列,依赖于主线程、主
全局并发队列(Global Dispatch Queue):GCD提供的默认并发队列。
- 为了方便程序员的使用,苹果提供了全局队列
- 在使用多线程开发时,如果对队列没有特殊需求,
在执行异步任务时,可以直接使用全局队列
- 使用
dispatch_get_global_queue
获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)
- 第一个参数表示
队列优先级
,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0
,在ios9之后,已经被服务质量(quality-of-service)
取代 - 第二个参数使用0
- 第一个参数表示
//全局并发队列的获取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
//优先级从高到低(对应的服务质量)依次为
- 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
函数与队列的不同组合
下面来看看不同的组合方式:
串行队列 + 同步函数
-(void) test01 {
NSLog(@"串行 + 同步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_get_main_queue();
// dispatch_queue_create("com.GCD.Queue", NULL);
for (int i = 0; i < 5; i++) {
NSLog(@"串行 + 同步:%d - %@", i, [NSThread currentThread]);
}
}
运行结果:
通过打印结果可以看出并没有开辟新的线程,任务一个接一个在当前线程中执行
串行队列 + 异步函数
-(void) test02 {
NSLog(@"串行 + 异步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 5; i++) {
dispatch_async(serialQueue, ^{
NSLog(@"串行 + 异步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
可以看到,任务还是一个接一个的执行,但是这里开辟了新的线程
并发队列 + 同步函数
-(void) test03 {
NSLog(@"并发 + 同步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 5; i++) {
dispatch_sync(serialQueue, ^{
NSLog(@"并发 + 同步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
并发队列 + 异步函数
-(void) test04 {
NSLog(@"并发 + 异步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 5; i++) {
dispatch_async(serialQueue, ^{
NSLog(@"并发 + 异步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
这里我们对比这来看并发队列使用同步、异步函数时的区别:
- 同步函数没有开辟新的线程
- 异步函数开辟了新的线程
在上文中我们已经说了并发队列的并发功能只能在异步函数中使用,这究竟是为什么呢?
- 首先同步函数会阻塞当前线程,直至任务完成
- 而异步函数是由系统线程池管理的,没不会阻塞当前线程,当任务位于队列中的时候,会直接分配给其他线程来执行,这就已经造就了两者之间的差别。
下面附一张图来帮助理解:
主队列 + 同步函数
-(void) test05 {
NSLog(@"主队列 + 同步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_get_main_queue();
for (int i = 0; i < 5; i++) {
dispatch_sync(serialQueue, ^{
NSLog(@"主队列 + 同步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
我们可以看到这里任务相互等待,造成了死锁。
原因分析
- 主队列有两个任务,顺序为:
NSLog任务
-同步block
- 执行
NSLog
任务后,执行同步Block
,会将任务1(即i=1时)加入到主队列,主队列顺序为:NSLog任务 - 同步block - 任务1
- 任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃
主队列 + 异步函数
-(void) test04 {
NSLog(@"主队列 + 异步:%@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_get_main_queue();
for (int i = 0; i < 5; i++) {
dispatch_async(serialQueue, ^{
NSLog(@"主队列 + 异步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
这里我们可以看到任务一个接一个的执行,没有开辟新的线程
全局并发队列 + 同步函数
-(void) test04 {
NSLog(@"全局并发队列 + 同步:%@", [NSThread currentThread]);
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"全局并发队列 + 同步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
任务按顺序一个一个执行,没有开辟新的线程
全局并发队列 + 异步函数
-(void) test04 {
NSLog(@"全局并发队列 + 异步:%@", [NSThread currentThread]);
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"全局并发队列 + 异步:%d - %@", i, [NSThread currentThread]);
});
}
}
运行结果:
任务乱序执行,开辟可新的线程
总结
函数与队列 | 串行队列 | 并发队列 | 主队列 | 全局并发队列 |
---|---|---|---|---|
同步函数 | 顺序执行,不开辟线程 | 顺序执行,不开辟线程 | 死锁 | 顺序执行,不开辟线程 |
异步函数 | 顺序执行,开辟线程 | 乱序执行,开辟线程 | 顺序执行,不开辟线程 | 乱序执行,开辟线程 |
死锁
死锁是指两个线程A和B都卡住了,A在等B,B在等A,相互等待对方完成某些操作。A不能完成是因为它在等待B完成。但B也不能完成,因为他在等待B完成。但B也不能完成,因为它在等待A完成。于是大家都完不成,导致了死锁。
死锁产生的四个必要条件
- 互斥访问,再有互斥访问的一个情况,线程才会等待
- 持有并等待,线程持有一些资源,并等待一些资源
- 资源非抢占:一旦一个资源被持有,除非持有者主动放弃,否则其他竞争者无法获取这个资源。
- 循环等待:循环等待是指存在一系列线程T0、T1…Tn,T0等待T1,T1等待T2,T2等待Tn,这样出现了一个循环。
dispatch_after
这是GCD提供的延迟执行函数,并非严格在指定时间后立即执行,而是将任务追加到目标队列的时间点。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"3秒后");
});
dispatch_once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_once
保证在APP运行期间,block中的代码仅仅执行一次,通常在单例以及方法交换中应用。
dispatch_apply
这是GCD提供给我们的一种快速迭代方法,该方法按照指定的次数将指定的任务追加到指定队列中去,等到全部的任务执行结束以后,系统会根据实际情况自动分配和管理线程 。
这里我们先来看一个使用其的例子
-(void) test06 {
NSLog(@"-- begin --");
NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];
dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT);
/*
arr.count 指定重复次数 这里数组内元素个数是 5 个,也就是重复 5 次
queue 追加任务的队列
index 带有参数的 Block, index 的作用是为了按执行的顺序区分各个 Block
*/
dispatch_apply(arr.count, queue, ^(size_t index) {
NSLog(@"index = %zu, str = %@ -- %@", index, arr[index], [NSThread currentThread]);
});
NSLog(@"-- end --");
}
运行结果:
这里我们可以看到,在dispatch_apply
中任务是并发执行的,但是会阻塞当前线程,直至所有任务全部结束以后才会打印end,所以我们可以在使用的时候给外面套上一层dispatch_async
来解决阻塞当前线程的问题。
应用场景:用来拉取网络数据后提前算出各个空间的大小,防止绘制时计算,提高表单滑动流畅性。
dispatch_group_t
dispatch_group_t
调度组将任务分组执行,能够监听任务组完成,并等待时间。
应用场景:多个接口请求之后的刷新页面
使用dispatch_group_async + dispatch_group_notify
-(void) test07 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"任务一:%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务二:%@", [NSThread currentThread]);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"刷新页面");
});
}
使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
-(void) test08 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务一:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务二:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
}
这里成组的使用dispatch_group_enter
以及dispatch_group_leave
可以令进出组的逻辑更加清晰。
在进出组的基础上使用 dispatch_group_wait
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务一:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务二:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
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(@"刷新页面");
});
栅栏函数
栅栏函数主要由两种使用场景:串行队列、并发队列。使用场景:同步锁
等栅栏前追加到队列中的任务执行完毕以后,再将栅栏后的人物追加到队列中去,简而言之就是先执行栅栏函数前的任务,而后在执行栅栏任务,最后执行栅栏后的任务。
串行队列使用栅栏函数:
-(void) test09 {
dispatch_queue_t queue = dispatch_queue_create("com.GCD.Queue", NULL);
NSLog(@"开始——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s执行的任务——%@", [NSThread currentThread]);
});
NSLog(@"第一次结束——%@", [NSThread currentThread]);
dispatch_barrier_async(queue, ^{
NSLog(@"--------栅栏任务-------------\n%@", [NSThread currentThread]);
});
NSLog(@"栅栏结束——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"延迟1s的任务——%@", [NSThread currentThread]);
});
NSLog(@"第二次结束——%@", [NSThread currentThread]);
}
运行结果:
栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1
、任务2
结论:由于串行队列异步执行
任务是一个接一个执行完毕的,所以使用栅栏函数没意义
并发队列使用栅栏函数
dispatch_queue_t queue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s执行的任务——%@", [NSThread currentThread]);
});
NSLog(@"第一次结束——%@", [NSThread currentThread]);
dispatch_barrier_async(queue, ^{
NSLog(@"--------栅栏任务-------------\n%@", [NSThread currentThread]);
});
NSLog(@"栅栏结束——%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"延迟1s的任务——%@", [NSThread currentThread]);
});
NSLog(@"第二次结束——%@", [NSThread currentThread]);
运行结果:
结论:由于并发队列异步执行
任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序
dispatch_barrier_sync/dispatch_barrier_async区别
dispatch_barrier_async
:前面任务执行完毕才会来到这里dispatch_barrier_sync
:作用相同,但是会堵塞线程,影响后面任务的执行
将上个案例中的dispatch_barrier_async
改为dispatch_barrier_sync
之后运行结果:
我们可以发现,dispatch_barrier_async可以控制队列中任务的执行顺序,而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)
栅栏函数注意点
- 1.尽量使用自定义的并发队列:
- 使用
全局队列
起不到栅栏函数
的作用 - 使用
全局队列
时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
- 使用
- 2.
栅栏函数只能控制同一并发队列
:打个比方,平时在使用AFNetworking
做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking
内部有自己的队列
dispatch_semaphore_t
信号量主要用作同步锁,用于控制GCD最大并发数
dispatch_semaphore_create()
:创建信号量dispatch_semaphore_wait()
:等待信号量,信号量减1
.当信号量< 0
时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)
才执行下去dispatch_semaphore_signal()
:释放信号量,信号量加1
.当信号量>= 0
会执行wait
之后的代码。
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("hello", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
NSLog(@"当前线程%d————线程%@", i, [NSThread currentThread]);
dispatch_semaphore_signal(sem);
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
上述代码展示的是一个使用信号量按序号输出的例子
如果当创建信号量时传入值为1又会怎么样呢?
i=0
时有可能先打印,也可能会先发出wait
信号量-1,但是wait
之后信号量为0不会阻塞线程,所以进入i=1
i=1
时有可能先打印,也可能会先发出wait
信号量-1,但是wait
之后信号量为-1阻塞线程,等待signal
再执行下去
设置最大开辟线程数
当我们下载图片的时候,进行并发异步,每一个下载都会开辟一个新的线程,这里我们担心太多线程会导致内存开销太大,以及线程的上下文切换会给我们的CPU带来的开销太大导致的问题,我们可以设置一个最大开辟线程数:
dispatch_semaphore_t currSingal = dispatch_semaphore_create(3);// 创建信号量,如果小于0则会返回NULL
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(currSingal, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务, %d %@", i, [NSThread currentThread]);
sleep(1);
NSLog(@"完成任务, %d %@", i, [NSThread currentThread]);
dispatch_semaphore_signal(currSingal);
});
}
这里的任务我们在执行的时候可以发现是一次三个三个的执行,即最多只开辟了三个线程。
但是这里我们打印NSTread的时候,会出现下面这些内容:
这个报错是由于这里出现了一个优先级反转的问题。
这里先介绍一下优先级反转:优先级:线程C>线程B>线程A。优先级较低的线程B,通过压制优先级更低的线程A,比高优先级的线程C先执行了。
解释:假如线程A拿到一个资源后加锁,线程C因为也需要这个资源于是挂起等待A执行结束。这一段符合逻辑没问题,但是此时线程B因为优先级比线程A高,直接抢占CPU,线程B执行完后,线程A执行,A解锁释放后,C再执行。这就导致原本优先级较低的线程B,通过压制线程A,比高优先级的线程C先执行了。
这里给一张经典的图: