概念
基础
https://github.com/ChenYilong/ParseSourceCodeStudy/blob/master/01_Parse的多线程处理思路/GCD扫盲篇.md
串行队列:同一时间队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。
你不知道在一个Block(任务)执行结束到下一个Block(任务)开始执行之间的这段时间时间是多长
并行队列:你唯一能保证的是,这些任务会按照被添加的顺序开始执行。但是任务可以以任何顺序完成
你不知道在执行下一个任务是从什么时候开始,或者说任意时刻有多个Block(任务)运行,这个完全是取决于GCD。
全局队列:不要与 barrier 栅栏方法搭配使用, barrier 只有与自定义的并行队列一起使用,才能让 barrier 达到我们所期望的栅栏功能。与 串行队列或者 global 队列 一起使用,barrier 的表现会和 dispatch_sync 方法一样
主队列:不能与 sync 同步方法搭配使用,会造成死循环
四个有名的队列
同步串行队列
不会新建线程,依然在当前线程上。
类似同步锁,是同步锁的替代方案
异步并发队列
会新建线程,可以开多条线程。
iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条。
异步串行队列
会新建线程,只开一条线程
一条线程就够了
每次使用 createDispatch 方法就会新建一条线程,多次调用该方法,会创建多条线程,多条线程间会并行执行
同步并行队列
不会新建线程,依然在当前线程上
串行队列中的同步与异步的区别
串行队列能确保顺序执行任务,他们两个的唯一区别在于dispatch_sync只会在 block 完全执行完之后返回,dispatch_async 不能确保会在 block 完全执行完之后返回,唯一能确定的是会在被添加到queue 队列后返回。
下面的代码:
dispatch_async(_serialQueue, ^{ printf("1"); });
printf("2");
dispatch_async(_serialQueue, ^{ printf("3"); });
printf("4");
可能会打印 2413 、 2143 、 1234 ,但有一点是可以确认的: 1 总是在 3 之前。
然而下面的代码:
dispatch_sync(_serialQueue, ^{ printf("1"); });
printf("2");
dispatch_sync(_serialQueue, ^{ printf("3"); });
printf("4");
总会打印:1234
队列和任务
队列
GCD有三种队列类型:串行队列,并发队列,主队列
串行队列:如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。
并发队列:并发队列会基于系统负载来合适地选择并发执行这些任务。并发队列一般指的就是全局队列(Global queue),进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。
主队列:与主线程功能相同。提交至main queue的任务会在主线程中执行。这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
并行执行的队列数量受到内核数的限制,无法真正做到大量队列并行执行;比如,对于并行队列中的全局队列而言,其存在优先级关系,执行的时候也会遵循其优先顺序,而不是并行。
任务
同步任务,使用dispatch_sync将任务加入队列。将同步任务加入串行队列,会顺序执行,一般不这样做,并且在一个任务未结束时调起其它同步任务会死锁。将同步任务加入并行队列,会顺序执行,但是也没什么意义。
异步任务,使用dispatch_async将任务加入队列。将异步任务加入串行队列,会顺序执行,并且不会出现死锁问题。将异步任务加入并行队列,会并行执行多个任务,这也是我们最常用的一种方式。
1.一些GCD的函数
dispatch_group
一般用法
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中多个异步任务执行完毕,做一些事情,介绍两种方式
// 方式1(不好,会卡住当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
...
// 方式2(比较好)
dispatch_group_notify(group, mainQueue, ^{
// 任务完成后,在主队列中做一些操作
...
});
上述的一种方式,可以适用于自己维护的一些异步任务的同步问题;但是对于已经封装好的一些库,比如AFNetworking等,我们不获取其异步任务的队列,这里可以通过一种计数的方式控制任务间同步,下面为解决单界面多接口的一种方式。
// 计数+1
dispatch_group_enter(group);
[JDApiService getActivityDetailWithActivityId:self.activityId Location:stockAddressId SuccessBlock:^(NSDictionary *userInfo) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
}];
// 计数+1
dispatch_group_enter(group);
[JDApiService getAllCommentWithActivityId:self.activityId PageSize:3 PageNum:self.commentCurrentPage SuccessBlock:^(NSDictionary *userInfo) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
} FailureBlock:^(NSError *error) {
// 数据返回后一些处理
...
// 计数-1
dispatch_group_leave(group);
}];
// 其实用计数的说法可能不太对,但是就这么理解吧。会在计数为0的时候执行dispatch_group_notify的任务。
dispatch_group_notify(group, mainQueue, ^{
// 一般为回主队列刷新UI
...
});
2.dispatch_barrier_async
// dispatch_barrier_async的作用可以用一个词概括--承上启下,它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。本例中,任务4会在任务1、2、3都执行完之后执行,而任务5、6会等待任务4执行完后执行。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 任务1
...
});
dispatch_async(queue, ^{
// 任务2
...
});
dispatch_async(queue, ^{
// 任务3
...
});
dispatch_barrier_async(queue, ^{
// 任务4
...
});
dispatch_async(queue, ^{
// 任务5
...
});
dispatch_async(queue, ^{
// 任务6
...
});
和dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,可以在比如文件的读写操作时使用,保证读操作的准确性。另外,有一点需要注意,dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
3.dispatch_apply
// for循环做一些事情,输出0123456789
for (int i = 0; i < 10; i ++) {
NSLog(@"%d", i);
}
// dispatch_apply替换(当且仅当处理顺序对处理结果无影响环境),输出顺序不定,比如1098673452
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*! dispatch_apply函数说明
*
* @brief dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API
* 该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执行结束
*
* @param 10 指定重复次数 指定10次
* @param queue 追加对象的Dispatch Queue
* @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block
*
*/
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
因为dispatch_apply并行的运行机制,效率一般快于for循环的类串行机制(在for一次循环中的处理任务很多时差距比较大)。比如这可以用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性,如果用for循环,耗时较多,并且每个表单的数据没有依赖关系,所以用dispatch_apply比较好。
4.dispatch_suspend和dispatch_resume
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_suspend(queue); //暂停队列queue
dispatch_resume(queue); //恢复队列queue
这两个函数不会影响到队列中已经执行的任务,队列暂停后,已经添加到队列中但还没有执行的任务不会执行,直到队列被恢复。
5.dispatch_semaphore_signal
// dispatch_semaphore_signal有两类用法:a、解决同步问题;b、解决有限资源访问(资源为1,即互斥)问题。
// dispatch_semaphore_wait,若semaphore计数为0则等待,大于0则使其减1。
// dispatch_semaphore_signal使semaphore计数加1。
// a、同步问题:输出肯定为1、2、3。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(1);
dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0);
dispatch_semaphore_t semaphore3 = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
// 任务1
dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);
NSLog(@"1\n");
dispatch_semaphore_signal(semaphore2);
dispatch_semaphore_signal(semaphore1);
});
dispatch_async(queue, ^{
// 任务2
dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER);
NSLog(@"2\n");
dispatch_semaphore_signal(semaphore3);
dispatch_semaphore_signal(semaphore2);
});
dispatch_async(queue, ^{
// 任务3
dispatch_semaphore_wait(semaphore3, DISPATCH_TIME_FOREVER);
NSLog(@"3\n");
dispatch_semaphore_signal(semaphore3);
});
// b、有限资源访问问题:for循环看似能创建100个异步任务,实质由于信号限制,最多创建10个异步任务。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
for (int i = 0; i < 100; i ++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
// 任务
...
dispatch_semaphore_signal(semaphore);
});
}
6.dispatch_set_context、dispatch_get_context和dispatch_set_finalizer_f
// dispatch_set_context、dispatch_get_context是为了向队列中传递上下文context服务的。
// dispatch_set_finalizer_f相当于dispatch_object_t的析构函数。
// 因为context的数据不是foundation对象,所以arc不会自动回收,一般在dispatch_set_finalizer_f中手动回收,所以一般讲上述三个方法绑定使用。
- (void)test
{
// 几种创建context的方式
// a、用C语言的malloc创建context数据。
// b、用C++的new创建类对象。
// c、用Objective-C的对象,但是要用__bridge等关键字转为Core Foundation对象。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
if (queue) {
// "123"即为传入的context
dispatch_set_context(queue, "123");
dispatch_set_finalizer_f(queue, &xigou);
}
dispatch_async(queue, ^{
char *string = dispatch_get_context(queue);
NSLog(@"%s", string);
});
}
// 该函数会在dispatch_object_t销毁时调用。
void xigou(void *context)
{
// 释放context的内存(对应上述abc)
// a、CFRelease(context);
// b、free(context);
// c、delete context;
}
死锁
dispatch_sync
// 假设这段代码执行于主队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 在主队列添加同步任务
dispatch_sync(mainQueue, ^{
// 任务
...
});
// 在串行队列添加同步任务
dispatch_sync(serialQueue, ^{
// 任务
...
dispatch_sync(serialQueue, ^{
// 任务
...
});
};
dispatch_apply
// 因为dispatch_apply会卡住当前线程,内部的dispatch_apply会等待外部,外部的等待内部,所以死锁。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, queue, ^(size_t) {
// 任务
...
dispatch_apply(10, queue, ^(size_t) {
// 任务
...
});
});
dispatch_barrier
dispatch_barrier_sync在串行队列和全局并行队列里面和dispatch_sync同样的效果,所以需考虑同dispatch_sync一样的死锁问题。
其他令人迷惑的问题
队列和线程的关系
错误理解:
有些人会产生一种错觉,觉得队列就是线程。又有些人会有另外一种错觉,一个追加Block就是一个线程。
正确理解:
对我们使用者来说,与其说GCD是面向线程的,不如说是面向队列的。 它隐藏了内部线程的调度。
我们所做的仅仅是创建不同的队列,把Block追加到队列中去执行,而队列是FIFO(先进先出)的。
它会按照我们追加的Block的顺序,在综合我们调用的gcd的api(sync、async、dispatch_barrier_async等等),以及根据系统负载来增减线程并发数, 来调度线程执行Block。
总结一下:
往主队列提交Block,无论是sync,还是async,都是在主线程中执行。
往非主队列中提交,如果是sync,会在当前提交Block的线程中执行。如果是async,则会在分线程中执行。
GCD的死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务一");
});
NSLog(@"任务二");
如上,在主线程中,往主队列同步提交了任务一。因为往queue中提交Block,总是追加在队列尾部的,而queue执行Block的顺序为先进先出(FIFO),所以任务一需要在当前队列执行完它之前所有的任务(例如任务二),才能轮到它被执行。(注意,这里引起死锁并不是因为任务二,哪怕删去任务二,这里仍然会死锁。这里只是为了举例说明,看很多人都在费解这一点,特此说明...)
而任务二因为任务一的sync,被阻塞了,它需要等任务一执行完才能被执行。两者互相等待对方执行完,才能执行,程序被死锁在这了。
这里需要注意这里死锁的很重要一个条件也因为主队列是一个串行的队列(主队列中只有一条主线程)。如果我们如下例,在并行队列中提交,则不会造成死锁:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务一");
});
NSLog(@"任务二");
});
原因是并行队列中任务一虽被提交仍然是在queue的队尾,在任务二之后,但是因为是并行的,所以任务一并不会一直等任务二结束才去执行,而是直接执行完。此时任务二的因为任务一的结束,sync阻塞也就消除,任务二得以执行。
上述第一个死锁的例子,我们很简单的改写一下,死锁就被消除了:
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务一");
});
NSLog(@"任务二");
我们在主线程中,往全局队列同步提交了Block,因为全局队列和主队列是两个队列,所以任务一的执行,并不需要等待任务二。所以等任务一结束,任务二也可以被执行。
当然这里因为提交Block所在队列,Block被执行的队列是完全不同的两个队列,所以这里用串行queue,也是不会死锁的。
sync的阻塞机制:
sync提交Block,首先是阻塞的当前提交Block的线程(简单理解下就是阻塞sync之后的代码)。例如我们之前举的例子中,sync总是阻塞了任务二的执行。
而在队列中,轮到sync提交的Block,仅仅阻塞串行queue,而不会阻塞并行queue。(dispatch_barrier_(a)sync除外,我们后面会讲到。)
我们了解了sync的阻塞机制,再结合发生死锁的根本原因来自于互相等待,我们用下面一句话来总结一下,会引起GCD死锁的行为:
如果同步(sync)提交一个Block到一个串行队列,而提交Block这个动作所处的线程,也是在当前队列,就会引起死锁。
以下4个GCD方法的区别
dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
dispatch_barrier_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
dispatch_barrier_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
1. dispatch_barrier_async
它的作用可以用一个词概括--承上启下,它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。当然它的作用导致它只有在并行队列中有意义。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 任务1
...
});
dispatch_async(queue, ^{
// 任务2
...
});
dispatch_async(queue, ^{
// 任务3
...
});
dispatch_barrier_async(queue, ^{
// 任务4
...
});
dispatch_async(queue, ^{
// 任务5
...
});
dispatch_async(queue, ^{
// 任务6
...
});
任务1,2,3的顺序不一定,4在中间,最后是5,6任务顺序不一定。它就像一个栅栏一样,挡在了一个并行队列中间。
当然这里有一点需要注意的是:dispatch_barrier_(a)sync只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
用途, 例如我们在一个读写操作中:
我们要知道一个数据,读与读之间是可以用线程并行的,但是写与写、写与读之间,就必须串行同步或者使用线程锁来保证线程安全。但是我们有了dispatch_barrier_async,我们就可以如下使用:
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
//读操作
});
dispatch_async(queue, ^{
// 读操作
});
dispatch_barrier_async(queue, ^{
// 写操作
});
dispatch_barrier_async(queue, ^{
// 写操作
});
dispatch_async(queue, ^{
// 读操作
});
2. dispatch_barrier_sync 和 dispatch_barrier_async 的区别
作用几乎一样,都可以在并行queue中当做栅栏。
dispatch_barrier_sync有GCD的sync共有特性,会阻塞提交Block的当前线程,而dispatch_barrier_async是异步提交,不会阻塞。
3. dispatch_sync 和 dispatch_barrier_sync 的区别
二者因为是sync提交,所以都是阻塞当前提交Block线程。
dispatch_sync并不能阻塞并行队列
dispatch_queue_t queue = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
dispatch_async(queue, ^{
NSLog(@"任务二");
});
dispatch_async(queue, ^{
NSLog(@"任务三");
});
//睡眠2秒
[NSThread sleepForTimeInterval:2];
NSLog(@"任务一");
});
输出结果 :
任务三
任务二
任务一
很显然,并行队列没有被sync所阻塞。
而dispatch_barrier_sync可以阻塞并行队列(栅栏作用的体现):
dispatch_queue_t queue = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_sync(queue, ^{
dispatch_async(queue, ^{
NSLog(@"任务二");
});
dispatch_async(queue, ^{
NSLog(@"任务三");
});
//睡眠2秒
[NSThread sleepForTimeInterval:2];
NSLog(@"任务一");
});
输出结果 :
任务一
任务二
任务三