GCD是iOS开发中常用的多线程工具,它是一套低层级的C API,通
过 GCD,开发者只需要向队列中添加一段代码块(block或C函数指针),而不需要直接和线程打交道。GCD也可以说是面向队列编程,将我们希望添加的同步、异步任务放到不同的队列中去执行。
本文目录
1、常见概念解析
2、同步异步函数使用方法探究
3、死锁发生的场景
4、常用API总结
一、常见概念解析
GCD的使用必定绕不开多线程,先来说一说多线程中的一些概念,可以帮助童鞋们更好的理解后面的内容,新手也可以借此扫盲
Dispatch Queue:执行处理的等待队列,开发者将block添加到队列当中,不需要关心线程的创建等过程(任务添加基于FIFO原则)。队列分为两种,一种是Serial Dispatch Queue(串行队列),另一种是Concurrent Dispatch Queue(并发队列)。
Serial Dispatch Queue:一次只能执行一个任务,后面的任务必须等待当前任务执行完毕才能开始执行
Concurrent Dispatch Queue:一次可以执行多个任务,后面的任务不需要等待当前的任务执行完毕
同步:同步任务创建后必须立即执行,并且堵塞当前线程,当该任务返回后线程继续处理后面的任务
异步:异步任务创建后立即返回,不会堵塞线程,后面的任务不必等待该任务返回,一般会新创建一个线程来处理这个任务
二、同步异步函数使用方法探究
只通过我的叙述可能还是有些迷惑,我们通过代码来帮助理解概念
1、同步任务+串行队列
- (void)syncSerial{
dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
});
}
第一种情况的打印结果
结论: 同步函数+串行队列,不创建新线程,顺序执行
2、 同步函数+并发队列
- (void)syncConcurrent{
dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
});
}
第二种情况的打印结果.png
结论:同步函数+并发队列 不开启新线程,顺序执行
3、异步函数+串行队列
- (void)asyncSerial{
dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
});
}
第三种情况的打印结果.png
结论: 异步函数+串行队列 开启新线程,顺序执行任务
4、 异步函数+并发队列
- (void)asyncConcurren{
NSLog(@"开始任务");
dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
});
}
第四种情况的打印结果.png
结论: 异步函数+并发队列 创建新线程,并发执行任务
通过以上实验可以看出,对于串行队列,任务都是顺序执行的,只不过异步任务会新创建一个线程,同步任务在当前线程,但是多个异步任务都在同一个新创建的线程中。
对于并发队列,同步任务不会创建新的线程,顺序执行,异步任务会创建多个子线程,而且是并行执行的。
在这里解释一下并发和并行的区别,并发是指在一个时间段内可以处理多个任务,但这些任务如果以微观视角来看还是顺序执行的,只不过CPU的处理速度极快,我们感觉不出来。而并行是真正的同时处理多个任务,可以想象是多条公路上同时有汽车在跑。
对于并发队列,虽然可以同时处理多个任务,但还是遵循FIFO原则的。如果最多只能创建四个子线程,分别正在执行四个任务,这时有第五个任务加入了队列,它由最先处理完任务的线程来处理,否则就得等待。也就是说从单个线程的处理过程看是串行的,也是FIFO的。同理多个串行队列同时执行任务也可以认为是并行的。
另外常用的API还有栅栏函数,它的功能用一句话总结:保证它之前的任务先于它执行,它后面的任务后于它执行,文艺点说是起到承上启下的作用。
- (void)asyncBarrier{
dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"读取任务1");
});
dispatch_async(queue, ^{
NSLog(@"读取任务2");
});
dispatch_async(queue, ^{
NSLog(@"读取任务3");
});
dispatch_barrier_async(queue, ^{
NSLog(@"写入");
});
dispatch_async(queue, ^{
NSLog(@"读取任务4");
});
dispatch_async(queue, ^{
NSLog(@"读取任务5");
});
dispatch_async(queue, ^{
NSLog(@"读取任务6");
});
}
栅栏函数的使用.png
通过执行结果可以看到,读取任务1、2、3的执行顺序不固定,但一定优于dispatch_barrier_async中的任务执行,读取任务4、5、6的执行顺序不固定,但一定迟于dispatch_barrier_async中的任务执行。就像栅栏一样将这些任务分割开。
dispatch_barrier_(a)sync使用时需要注意,它只对我们自己创建的队列有作用,对于系统的队列(主队列、全局队列)和dispatch_(a)sync功能相同。
三、死锁发生的场景
造成死锁的四个必要条件:
互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
在iOS开发中发生死锁,一般情况下资源是不可抢占的,而且既然造成了死锁那我们分析的重点在于是如何发生相互等待的。
在viewDidLoad方法中添加如下代码,这是在主队列中添加同步任务造成的死锁
主线程死锁.png
原因就在于viewDidLoad方法的代码块中是一个任务(调用时需要等待返回所以是同步任务,只是个人理解),是系统添加到主队列中的,而在该任务又将图中的一个block(可以看做另一个任务)添加到了主队列中,而且是个同步任务,该任务默认添加到队列尾部。根据之前的概念分析,主队列是一个特殊的串行队列,而且只包含主线程,因此一次只能处理一个任务,后面的任务必须等待当前任务执行完毕才可以执行。block中的任务等待viewDidLoad中的任务执行,而viewDidLoad任务以同步方式添加了block中的任务,需要等待同步任务返回才能继续执行,造成了循环等待。
如果你还是不明白,请看下面的例子
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
NSLog(@"2----当前线程%@",[NSThread currentThread]); // 任务2
//在当前block这个任务里面创建同步任务,是希望等待该任务返回后再执行后面的内容,即便后面没有代码执行,但是刚添加的任务在队列尾部,等待队列中靠前的任务执行。
dispatch_sync(queue, ^{
NSLog(@"3----当前线程%@",[NSThread currentThread]); // 任务3
});
NSLog(@"4----当前线程%@--%d",[NSThread currentThread],count); // 任务4
});
NSLog(@"5"); // 任务5
上面这段代码乍一看没什么问题,但是一运行就会报错,不信的小伙伴可以试一试
同步任务添加不当同步任务添加不当打印情况.png
我们来仔细分析一下上面这段代码这些任务的执行顺序,这样就可以找到到底是哪个地方在循环等待
首先在主线程中创建了一个串行队列,然后执行任务一,将异步任务添加到串行队列中马上返回(因为是异步所以系统会帮我们创建一个子线程来执行block里面的任务),执行任务五,异步任务在新的子线程中执行任务二,因此任务五和任务二的执行顺序不确定。后面就没有主队列的什么事了,我们创建的串行队列中是执行的子线程任务,不会牵扯到主线程。
任务二执行完毕后,将同步任务添加到串行队列,默认添加到队列尾部,但是在这里为什么不会执行任务三而是发生死锁呢。这说明任务三在等待它之前的任务执行,但恰好被等待的任务同步添加了任务三又在等待任务三执行并返回,所以造成了死锁(划重点,判断死锁的思路)。所以我们要找到是哪个任务在任务三前面并且同步添加了任务三。
其实这里需要仔细区分一下任务这个概念,GCD在添加任务时是以block为单位的,也就是说一个block内的代码块就是一个任务(即使代码块为空也是一个任务,大家可以想一想为什么viewDidLoad里面只有同步往主队列添加任务的代码也会造成死锁了),而我们通常打印的这些任务一、任务二这些是block内的子任务,也可以说是对block内任务的一个划分,只是为了方便描述。回到我们的代码,在串行队列中异步添加的block就是一个任务,当在处理这个任务的时候,它又申请添加了同步任务任务三,需要等待任务三的返回,但是任务三在队尾,等待block任务执行,所以造成了循环等待。
到此为止就给大家分析完了,当然造成死锁还会有其他情况,不过我暂时没有遇到,我认为只要分析透彻几种典型情况并掌握方法,在以后遇到问题时就不会手足无措了。
后面是一些常用的API,喜欢的童鞋可以拿走。
四、常用API总结
/*
* Main Dispatch Queue 的获取方法
*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
* Global Dispatch Queue (高优先级)的获取方法
*/
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
/*
* Global Dispatch Queue (默认优先级)的获取方法
*/
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* Global Dispatch Queue (低优先级)的获取方法
*/
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
/*
* Global Dispatch Queue (后台优先级)的获取方法
*/
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
// 一个异步的任务,例如网络请求,耗时的文件操作等等
...
dispatch_async(dispatch_get_main_queue(), ^{
// UI刷新
...
});
});
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的任务,例如创建单例
...
});
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
});
// 等待group中多个异步任务执行完毕,做一些事情
dispatch_group_notify(group, mainQueue, ^{
// 任务完成后,在主队列中做一些操作,相当于做完所有事情后总结,总是在最后执行,和这段代码的添加顺序无关
...
});
参考文章
GCD容易让人迷惑的几个小问题
iOS GCD全析(一)(这一系列的文件简直就是我的扫盲篇,大赞作者大大,希望可以帮到像我这样的小白)
网友评论