概述
说到多线程,对于做iOS的来说,基本上是再熟悉不过的了,多线程以极其高效率的执行代码任务的方式,贯穿于我们项目当中的各个模块.而在整个多线程的体系中,GCD可以说是多线程中的中流砥柱,也是我们绕不过去的一个重点话题.
什么是GCD?
让我们来看看苹果对其的描述 : Grand Central Dispatch (GCD)是异步执行任务的技术之一,一般将程序应用中的线程管理用的代码在系统级中实现.开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务.由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可以执行任务,这样就比以前的线程更有效率.
我们先来看一个例子:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
/**
执行耗时操作(如下载图片等)
*/
dispatch_async(dispatch_get_main_queue(), ^{
/**
回到主线程执行UI操作
*/
});
});
这个例子我们并不陌生,这里引申出在GCD中非常重要,但特别容易让人混淆的两个概念:
- 1 . 同步函数和异步函数
- 2 . 串行队列和并行队列
1 . 同步(sync)函数和异步(async)函数
首先应该弄清楚的是,同步函数和异步函数是相对于线程来说的,同步函数在提交任务时,会阻塞当前线程,而异步函数提交任务不会阻塞当前线程,这也佐证了sync无法开启新线程,而async可以.
2 . 串行(serial)队列和并行(concurrent)队列
串行队列和并行队列是相对与队列来说的,其实更确切一点说是相对任务来说的,不同的队列决定了任务是按顺序依次执行,还是没有顺序任意执行,串行队列会等待当前任务执行完,再执行后面的任务,而并行队列则不会等待,当前任务执行的同时后面的任务也会开始执行,无论是并行队列还是串行队列,都遵循FIFO(先进先出)原则
所谓任务,就是我们使用GCD时,提交的那个block
先用一个例子来解释一下串行和并行队列的不同之处:
dispatch_async(queue, block0);
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
dispatch_async(queue, block4);
dispatch_async(queue, block5);
dispatch_async(queue, block6);
dispatch_async(queue, block7);
如果这里的queue是串行队列,因为要等待当前任务执行完,所以先执行block0,接着block1...,依次执行
如果这里的queue是并行队列,无需等待block0执行完,所以在执行block0同时也会执行block1,block2...
获取串行队列和并行队列
一般来说,我们获取队列有两种方式,一种是直接获取系统的,另外一种是自己创建
- 1 . 系统提供
// 全局并行队列
dispatch_get_global_queue()
//主队列(存在于主线程中特殊的串行队列)
dispatch_get_main_queue()
- 2 .自己创建
// 串行队列 : DISPATCH_QUEUE_SERIAL换成NULL也是一样
dispatch_queue_create(@"com.queue.serial", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_create(@"com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
下面我用具体的案例来分析,加深理解(为了方便,假设我们的代码都是在主线程中
)
- 案例一:
NSLog(@"任务1");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
输出结果如下
sample1.png分析:首先打印任务1,接着遇到同步函数,阻塞线程,将任务加入到全局队列中,执行任务2,回到主线程中继续执行任务3
- 案例二:
NSLog(@"\n");
NSLog(@"任务1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
输出结果如下
sample2.png
分析:打印换行符是为了看打印结果,请忽略
,首先执行任务1,接着遇到异步函数,不会阻塞当前线程,继续执行,由于async可以开启新线程,也无需等待,所以任务2和任务5是无序的,然后遇到同步函数,阻塞当前线程,回到主线程中执行任务3,之后才能再继续执行任务4.
- 案例三:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
});
NSLog(@"任务4");
while (1) {}
NSLog(@"任务5");
输出结果如下
sample3.png
分析:开始遇到异步函数,不阻塞也不会等待,async开启新线程,所以任务1和任务4是无序的(注意此时主线程已被循环卡死了
),接着遇到同步函数,将任务2加入到主线程中,根据FIFO原则,此时加入的任务需要放到循环后面去执行,但是同步函数需要等待任务2执行完才会继续执行任务3,任务2又必须等待循环执行完再执行,所以就相互卡死在这里了,任务5自然不必说,有循环在,永远不会执行它.
以上分析的实例都在这里可以找到,我分析的有些地方与博主不同,请注意差异.
3 . GCD死锁(同样假设代码是在主线程中
)
先解释一下什么是GCD死锁:所谓GCD死锁,就是在一个串行队列中,任务A执行要等待任务B执行完才能执行,但是任务B也在等待任务A执行完,两者相互等待,最后导致都不能执行任务的一种场景(上文案例3其实也可以算是一种死锁
)
先看最常见的死锁:
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务1");
});
分析:(首先我们要明白的一点是,这个dispatch执行完的标志是任务1打印了,也就是说代码执行到了最后一个花括号这里)
.我们主线程的主队列中有一个任务,就是上述所有代码,然后接着往主队列中添加任务1,因为主队列是一个特殊的串行队列,所以任务1需要放到上述代码后面去执行,并进入等待,等待上述代码执行完再执行任务1,但是上述代码执行完的标志是任务1打印,所以上述代码在等待任务1执行完,任务1又在等待上述代码执行完.你等我执行,我等你执行,oh,my god.
这里有个比较有趣的问题,假如代码是如下这样:
dispatch_queue_t serialQueue = dispatch_queue_create("com.cib.serialQueue", NULL);
dispatch_sync(serialQueue, ^{
NSLog(@"任务任务你快打印啦...");
});
一个同步函数,一个串行队列,会不会造成死锁的现象呢?
直接看结果吧:
打印结果.png
相信有小伙伴对这个结果感到有点疑惑,一起分析一下:
首先是同步函数,这段代码会放在主线程执行,并等待执行结果,但是之前我们说,主线程中有一个特殊的串行队列,就是主队列,整段代码是放到主队列中执行的,但是我们在这里是把任务加到我们自己创建的串行队列中去执行的,这两个不同的队列各自只执行自己的那个任务,没有你等我,我等你这个逻辑在,参照死锁的概念,就不难理解这个结果了.
同步函数和自己创建的串行队列也有死锁,就是下面这种情况:
dispatch_queue_t queue = dispatch_queue_create("com.queue.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"任务1");
});
});
分析:首先遇到异步函数,将任务加入到串行队列中,然后遇上同步函数,阻塞当前线程,将任务追加到串行队列后面,串行队列中已经有了一个任务了,就是这段代码:
^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"任务1");
});
}
追加的任务是这段代码:
^{
NSLog(@"任务1");
}
现在阻塞了当前线程,需要等待追加的任务执行完,才能继续执行,追加的任务又在等第一个任务执行完才能执行,第一个任务执行完的标志是追加的任务执行了(也就是打印完任务1
),这不又卡死了.所以索性让你们两者在这千丝万缕的关系中,各自互相伤害去吧!
4 . 常见GCD用法
- 1 . dispatch after
经常会遇到这样的情况,我们希望任务在推迟一段时间后再执行,这样的场景dispatch after就非常合适了(当然除此之外你还可以使用performSelector: withObject: afterDelay:
)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"print in coming 3s");
});
使用比较简单,这里第一个参数是dispatch_time_t类型,创建dispatch_time_t需要指定从什么时候开始,还有时间间隔,第二个参数是队列,需要注意的是dispatch after并不是在指定时间后执行,而是在指定时间后追加到队列中去
- 2 . dispatch_group
有这么一种业务需求,需要执行几种任务,然后等待所有任务都执行完之后,再做一个统一的操作.比如说我们需要下载几张图片,等到所有图片都下载完成之后,再拼接在一起组合成一张新的图片.这时候一种可行的方式是把任务加到一个串行队列中去,让下载操作一个一个进行,目的是可以达到的,但是就体验和效率方面来讲,是不可取的,但是我们又不能仅仅只把任务追加到并行队列中之后就不管了,因为这样虽然效率和体验方面改善了,但是不可控,就是说我们不知道什么时候,任务都完成了,所以这根本就达不到我们的需求,在这个时候或许你可以考虑一下dispatch_group, dispatch_group使用方法也比较简单,如下:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
int count = 1000;
dispatch_group_async(group, queue, ^{
for (int i = 0; i < count; i++) {
if (i == count - 1) {
NSLog(@"task1 is completion");
}
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < count; i++) {
if (i == count - 1) {
NSLog(@"task2 is completion");
}
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < count; i++) {
if (i == count - 1) {
NSLog(@"task3 is completion");
}
}
});
dispatch_group_notify(group, queue, ^{
NSLog(@"all task is completion");
});
打印结果:
group.png可以看到最后打印的一定是all task is completion
,其他的三个任务是无序的.这完全可以解决上面我们所提的问题
- 3 . dispatch once
这个GCD方法是作用是让加入的任务只执行一次.这个方法一个最常见应用的场景就是我们设计模式中的单例模式了.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"这里只会打印一次");
});
- 4 . dispatch barrier
在日常工作当中,不可避免的会使用到数据库,当多线程与数据库碰到一起,就产生了线程安全问题,当然仅仅是读取操作是不会有影响的,但是一边写入,一边读取呢?串行队列是可以达到目的,但是老调重弹的一个问题是效率太低,那么有没有一种方法能达到读取随意,但是可以控制写入时只写入,不支持读取呢?答案就是dispatch barrier.我们看一下实例:
NSLog(@"\n");
dispatch_queue_t queue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"task1 for reading");
});
dispatch_async(queue, ^{
NSLog(@"task2 for reading");
});
dispatch_async(queue, ^{
NSLog(@"task3 for reading");
});
dispatch_async(queue, ^{
NSLog(@"task4 for reading");
});
dispatch_barrier_async(queue, ^{
NSLog(@"task8 for writing");
});
dispatch_async(queue, ^{
NSLog(@"task5 for reading");
});
dispatch_async(queue, ^{
NSLog(@"task6 for reading");
});
dispatch_async(queue, ^{
NSLog(@"task7 for reading");
});
打印结果:
barrier.pngdispatch barrier前面的任务和后面的任务是没有顺序的,但是dispatch barrier是有顺序的,就是一定在1,2,3,4之后,在5,6,7之前执行.
注: dispatch barrier只对自己创建的并行队列有效,对从系统获取的全局队列没有效果,要注意这个坑.
GCD常见用法就先列举这么多了,相信经过上文这么长的描述,小伙伴对GCD应该有了更深刻的理解了,其实GCD也没有想象中那么神秘,只要慢慢探索,还是可以理解的,套用社会主义老大的一句话就是:2017不要怂,撸起袖子加油干.当然如果在阅读过程中发现有错误的地方,欢迎指正
.
文中所有的案例都在这里,请戳demo地址
网友评论