首先先引用阳神Sunny博客中的几道面试题:
GCD开发中用的十分广泛,所以有必要进行深入的了解。下面就一步一步的深入下去。
概述
说到GCD自然就会想到多线程,GCD是一种异步执行任务的技术,它避免了让程序员直接操作线程的种种麻烦。在GCD中开发者只需要定义一系列的任务放到合适的运行队列中执行即可,这样GCD就会根据情况开若干条线程同时负责线程的生命周期。
队列
GCD只有两种类型的队列:
DISPATCH_QUEUE_SERIAL
串行队列
DISPATCH_QUEUE_CONCURRENT
并行队列
- 两种队列都是
dispatch_queue_t
类型的对象。
可以通过如下方法创建(第一个参数用来标识队列方便调试时候查看)。
dispatch_queue_create("com.longjianjiang.queue", DISPATCH_QUEUE_CONCURRENT);
- 两种队列的执行方式都是按照先进先出的原则,只是串行队列一次只执行一个任务,而并行队列在资源允许的情况下会开线程一次执行多个任务。
两个特殊的队列
-
dispatch_get_global_queue(long identifier, unsigned long flags)
系统提供的全局并行队列,可以指定优先级,一般默认选择DISPATCH_QUEUE_PRIORITY_DEFAULT
. -
dispatch_get_main_queue()
系统提供的主队列(串行队列),也就是提交的任务会在主线程执行.一般更新UI相关会用到主队列。
队列优先级
默认我们通过dispatch_queue_create
方法创建的队列优先级默认是DISPATCH_QUEUE_PRIORITY_DEFAULT
。如果想设置队列的优先级有两种方法。
- 1.
dispatch_queue_attr_make_with_qos_class
,如下图:
该方法通过设置
dispatch_queue_attr_t
来设置队列的优先级。
第一个参数
dispatch_queue_attr_t attr
:与特定的服务质量类相关联的队列的属性值信息。如果你想让被提交的任务被连续的执行,则指定DISPATCH_QUEUE_SERIAL值,或如果你想让被提交的任务被并发的执行,则指定DISPATCH_QUEUE_CONCURRENT值。如果你传NULL,则此方法默认创建一个连续的队列。
第二个参数dispatch_qos_class_t os_class
:和队列优先级dispatch_queue_priority_t
类似,同样有四种,具体和队列优先级的映射见下图。
第三个参数int relative_priority
:对第二个参数四个特定的服务质量优先级所代表的值的一个偏移,这个值必须不大于于0并且不小于QOS_MIN_RELATIVE_PRIORITY
,否则返回为NULL
.一般默认为0。
- 2.
dispatch_set_target_queue
方法设置优先级
第一个参数
dispatch_object_t object
: 要修改的队列,这个参数不能为NULL
。
第二个参数dispatch_queue_t queue
:有优先级的队列,执行完方法,前一个没有优先级的队列优先级和此队列相同。
改变多个队列任务的执行顺序
dispatch_set_target_queue
如果我们需要把不同队列中得不同任务按照顺序去执行,例如图中的
queue1
和queue2
分别存放两个任务,此时要求输出必须为2134,所以调用dispatch_set_target_queue
方法让queue1
和queue2
分别指定目标为串行队列consultQueue
,此时原本应该并行执行的四个任务只能一个一个依次执行。
执行方式
GCD只有两种执行方式
dispatch_sync
同步执行
dispatch_async
异步执行
- 同步执行就是多个任务依次按顺序执行,一个接着一个的执行。
- 异步执行就是在执行某个任务的时候,不等任务结束就可以返回,其他任务依然可以继续,也就是说异步执行通常会开新线程。
比如下载一张图片显示,要先从网络上下载图片,然后更新UI。同步方法就是等待图片下载完成再更新UI,而异步则是立刻从图片下载的方法返回并向后执行,此时我们依然可以处理界面上的点击事件,否则主线程就被阻塞了。
队列和执行方式的组合
所有组合及情况见下图:
注意第一种不能用的情况是当前线程在主线程,如果是非主线程的话则是可以的
// 该方法对当前线程进行判断,从而避免的死锁的发生
void runOnMainQueueWithoutDeadlocking(void (^block)(void))
{
if ([NSThread isMainThread])
{
block();
}
else
{
dispatch_sync(dispatch_get_main_queue(), block);
}
}
死锁问题
下图会导致死锁,为什么?
主线程是串行的,在执行某一个任务的时候线程被阻塞了,而这个任务(dispatch_sync)在执行时,又要求阻塞主线程,从而导致了互相的阻塞,也就是死锁。
避免死锁
除了特定要求需要同步执行,那么我们没有理由不充分利用CPU选择异步执行。
dispatch_queue_set_specific
,dispatch_queue_get_specific
,dispatch_get_specific
配合使用可以防止在串行队列中的同步任务嵌套一个此队列的同步任务从而导致死锁。
不过上面仅仅是为了举例,实际中并没有用过,一个比较好的例子就是FMDB中就用了此方法防止死锁的。
注意:如果不在队列中想要通过key获取到context,得使用
dispatch_queue_get_specific
传入参数队列才能获取。
dispatch_get_current_queue
此方法iOS6中被废弃了,为什么呢?
首先如果队列调用了dispatch_set_target_queue
方法
dispatch_set_target_queue(queue, targetQueue);
1.此时如果调用dispatch_get_current_queue
,是应该返回queue
还是targetQueue
呢?
2.如下图,通过dispatch_get_current_queue
方法判断当前队列是否为queueA
,如果不是就同步执行一个任务。
if (queueA == dispatch_get_current_queue()){
block();
} else {
dispatch_sync(queueA,block);
}
例如同步执行的block如下所示
dispatch_sync(queueB, ^{
//此时通过`dispatch_get_current_queue`得到的队列是`queueB`
//但此时`queueA`是被阻塞的,
//所以继续执行下面任务就会死锁。
dispatch_sync(queueA, ^{
// some task
});
});
所以dispatch_set_target_queue
使用不当会导致死锁,我们可以使用之前的dispatch_queue_get_specific
来实现相关功能。
附苹果文档的解释:
Recommended for debugging and logging purposes only: The code must not make any assumptions about the queue returned, unless it is one of the global queues or a queue the code has itself created. The code must not assume that synchronous execution onto a queue is safe from deadlock if that queue is not the one returned by dispatch_get_current_queue().
三种特殊的执行
-
dispatch_once
一次执行,大多用来创建单例或者全局的数据。
+ (UIColor *)color {
static UIColor *color;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
color = [UIColor orangeColor];
});
return color;
}
-
dispatch_after
延迟执行,不过blcok中任务不可以取消,所以建议如果可以的话使用-viewWillAppear
、-viewDidAppear
会更好。
-
dispatch_apply
类似for循环的一个方法,按指定的次数将指定的block
追加到指定的队列中,并等到全部的处理执行结束,默认同步执行,所以传入的队列不能为主队列,否则会死锁。但当传入的时全局队列的时候,执行是异步的 。 同时只有当执行完对应的次数后才会执行下面的代码,所以最后才输出 done。
dispatch groups
开发中我们的应用通常会向服务器发送一连串的请求,比如说应用启动的时候会向服务端请求一些配置信息,这些配置信息可能需要多个请求组合而成,而且这些请求彼此之间并没有关联,那么这个时候问题来了,我们如何知道这些任务什么时候执行完成了呢?
此时你就需要创建一个dispatch_group_t
:
dispatch_group_t group = dispatch_group_create();
下面我们可以将之前的任务添加到group
中:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
//需要执行的任务
});
但是当有些任务异步执行,会马上返回,这个时候group
就会认为放到group
中的任务已经结束,显然不合理。
这个时候我们可以通过dispatch_group_enter
表示要开始某个任务了,结束任务之后需要调用dispatch_group_leave
来退出group
。
dispatch_group_enter(group);
[service startWithCompletion:^(response *results, NSError* error){
// 需要执行的任务
dispatch_group_leave(serviceGroup);
}];
最后告诉group
任务执行完成
- 第一种方式:
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
第二个参数timeout
表示需要等待的时间,系统定义了两个常用的值DISPATCH_TIME_NOW
、 DISPATCH_TIME_FOREVER
,
如果使用了第一个值,表示会立即查看是否完成任务,第二个表示会等待任务全部结束。此时会阻塞当前的线程,直到dispatch group中的所有任务完成才会返回.
返回值如果是0表示group
中的任务执行结束,否则就不为0.
- 第二种方式
dispatch_group_notify(group, queue, ^{
//不会阻塞当前线程
});
两种方式按需求选择即可。
Using Barriers
在进行文件读和写或者数据库操作的时候,我们必须保证写数据的时候和修改数据库的时候有且仅有一个线程在操作,此时GCD提供了一个好的方法避免写冲突。
dispatch_barrier_async
用于等待前面的任务执行完毕后自己才执行,而它后面的任务需等待它完成之后才执行。
dispatch_barrier_sync
也可以实现上述功能
不过我们发现输出2222222222222的位置两者不一样,这是因为dispatch_barrier_sync
会阻塞当前线程,而dispatch_barrier_async
则不会。
Dispatch Semaphore
信号量也是用来处理当多个线程对某个资源更新可能产生数据的误操作。
信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时,计数会减少,当信号量为0,线程会被阻塞。
在GCD中有三个函数是semaphore的操作,分别是:
dispatch_semaphore_create
创建一个semaphore
dispatch_semaphore_signal
发送一个信号
dispatch_semaphore_wait
等待信号
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArrayarray array];
for (int index = 0; index < 100000; index++) {
dispatch_async(queue, ^(){
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//
NSLog(@"addd :%d", index);
[array addObject:[NSNumber numberWithInt:index]];
dispatch_semaphore_signal(semaphore);
});
}
注意:
-
dispatch_semaphore_wait
的第二个参数和之前的dispatch_group_wait
是一样的。返回值如果是0,说明此时信号量大于等于1,可以执行任务,非0的话则说明已处于阻塞状态。 - 当执行完操作之后应该调用
dispatch_semaphore_signal
方法,以便其他任务有机会去执行。
Dispatch 其他
-
dispatch_suspend
和dispatch_resume
dispatch_suspend
挂起指定的Dispatch Queue。
dispatch_resume
恢复指定的Dispatch Queue。
两者对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使这些处理能够继续执行。 -
dispatch_main
该方法可以阻塞主线程,同时必须只能在主线程中调用,否则会导致程序崩溃。
最后,面试题的答案都有了!
尾巴
欢迎关注@longjianjiang,下次再见。