GCD

再次学习GCD的内容,并总结。

GCD

GCD概要

介绍
异步执行的技术之一,开发者只需要定义向执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。

1
2
3
4
5
dispatch_async(queue,^{
dispatch_async(dispatch_get_main_queue(),^{
//主线程可以执行的处理:用户页面更新
});
});

多线程, 并发
一个应用就相当于一个进程, 而一个进程可以同时分发几个线程同时处理任务.而并发正是一个进程开启多个线程同时执行任务的意思, 主线程专门用来刷新UI,处理触摸事件等 而子线程呢, 则用来执行耗时的操作, 例如访问数据库, 下载数据等..
1个CPU执行的CPU命令列为一条无分叉路径,即为“线程”。
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU能够并列执行多个线程一样。

GCD的优势
说到优势, 当然有比较, 才能显得出优势所在. 事实上, iOS中我们能使用的多线程管理技术有

pthread(来自Clang, 纯C语言, 需要手动创建线程, 销毁线程, 手动进行线程管理. 而且代码极其恶心, )
NSThread(Foundation框架下的OC对象, 依旧需要自己进行线程管理,线程同步。 线程同步对数据的加锁会有一定的开销。)
GCD(两个字, 牛逼, 虽然是纯C语言, 但是它用难以置信的非常简洁的方式实现了极其复杂的多线程编程, 而且还支持block内联形式进行制定任务. 简洁! 高效! 而且我们再也不用手动进行线程管理了.)
NSOperationQueue(相当于Foundation框架的GCD, 以面向对象的语法对GCD进行了封装. 效率一样高)

以下优点:

GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。

GCD的API

在介绍GCD的API之前, 我们先搞清楚四个名词: 串行, 并行, 同步, 异步

串行 : 一个任务执行完, 再执行下一个任务
并行 : 多个任务同时执行
同步 : 在当前线程中执行任务, 不具备开启线程的能力
异步 : 在新的线程中执行任务, 具备开启线程的能力

Dispatch Queue
Dispatch Queue是执行处理的等待队列, 按照先进先出(FIFO, First-In-First-Out)的顺序进行任务处理.队列分两种.
一种是串行队列(Serial Dispatch Queue),
一种是并行队列(Concurrent Dispatch Queue).

Dispatch Queue的种类            说明  
Serial Dispatch Queue            等待现在执行中处理结束  
Concurrent Dispatch Queue       不等待现在执行中处理结束

并发队列 : 让多个任务同时执行(自动开启多个线程执行任务)
并发功能只有在异步函数(dispatch_async)下才有效(想想看为什么?)

创建队列

dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
手动创建一个队列.

label : 队列的标识符, 日后可用来调试程序
attr : 队列类型
DISPATCH_QUEUE_CONCURRENT : 并发队列
DISPATCH_QUEUE_SERIAL 或 NULL : 串行队列

需要注意的是, 通过dispatch_queue_create函数生成的queue在使用结束后需要通过dispatch_release函数来释放.(只有在MRC下才需要释放)

并不是什么时候都需要手动创建队列, 事实上系统给我们提供2个很常用的队列.

主队列
dispatch_get_main_queue();
该方法返回的是主线程中执行的同步队列. 用户界面的更新等一些必须在主线程中执行的操作追加到此队列中.

全局并发队列
dispatch_get_global_queue(long identifier, unsigned long flags);
该方法返回的是全局并发队列. 使用十分广泛.

identifier : 优先级
DISPATCH_QUEUE_PRIORITY_HIGH : 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT : 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW : 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND : 后台优先级
flags : 暂时用不上, 传 0 即可

注意 : 对Main Dispatch Queue和Global Dispatch Queue执行dispatch_release和dispatch_retain没有任何问题. (MRC)

同步函数
dispatch_sync(dispatch_queue_t queue, ^(void)block);
在参数queue队列下同步执行block

异步函数
dispatch_async(dispatch_queue_t queue, ^(void)block);
在参数queue队列下异步执行block(开启新线程)
dispatch_async 添加一个 Block 到队列就立即返回了。任务会在之后由 GCD 决定执行。当你需要在后台执行一个基于网络或 CPU 紧张的任务时就使用 dispatch_async ,这样就不会阻塞当前线程。

时间
dispatch_time(dispatch_time_t when, int64_t delta);
根据传入的时间(when)和延迟(delta)计算出一个未来的时间

when :
DISPATCH_TIME_NOW : 现在
DISPATCH_TIME_FOREVER : 永远(别传这个参数, 否则该时间很大)
delta : 该参数接收的是纳秒, 可以用一个宏NSEC_PER_SEC来进行转换, 例如你要延迟3秒, 则为 3 * NSEC_PER_SEC.

延迟执行
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);

有了上述获取时间的函数, 则可以直接把时间传入, 然后定义该延迟执行的block在哪一个queue队列中执行.
苹果还给我们提供了一个在主队列中延迟执行的代码块, 如下

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
code to be executed after a specified delay
});

我们只需要传入需要延迟的秒数(delayInSeconds)和执行的任务block就可以直接调用了, 方便吧~

注意 : 延迟执行不是在指定时间后执行任务处理, 而是在指定时间后将处理追加到队列中, 这个是要分清楚的.还是在主队列做这些操作吧。

队列组 (调度组)
Dispatch Group 会在整个组的任务都完成时通知你。这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。而且在整个组的任务都完成时,Dispatch Group 可以用同步的或者异步的方式通知你。因为要监控的任务在不同队列,那就用一个 dispatch_group_t 的实例来记下这些不同的任务。

当组中所有的事件都完成时,GCD 的 API 提供了两种通知方式。
第一种是 dispatch_group_wait,它会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时发生。这恰好是你目前所需要的
第二章是dispatch_group_notify,以异步的方式工作。当 Dispatch Group 中没有任何任务时,它就会执行其代码,那么 completionBlock 便会运行。你还指定了运行 completionBlock 的队列,此处,主队列就是你所需要的。

Dispatch Source
第一次使用 Dispatch Source 可能会迷失在如何使用一个源,所以你需要知晓的第一件事是 dispatch_source_create 如何工作。下面是创建一个源的函数原型:

1
2
3
4
5
6
dispatch_source_t dispatch_source_create(
dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t queue);
}

第一个参数是 dispatch_source_type_t 。这是最重要的参数,因为它决定了 handle 和 mask 参数将会是什么。你可以查看 Xcode 文档 得到哪些选项可用于每个 dispatch_source_type_t 参数。

下面你将监控 DISPATCH_SOURCE_TYPE_SIGNAL 。如文档所显示的:

一个监控当前进程信号的 Dispatch Source。 handle 是信号编号,mask 未使用(传 0 即可)。

dispatch_group_create();
有时候我们想要在队列中的多个任务都处理完毕之后做一些事情, 就能用到这个Group. 同队列一样, Group在使用完毕也是需要dispatch_release掉的(MRC).

栅栏
dispatch_barrier_async(dispatch_queue_t queue, ^(void)block)
GCD 通过用 dispatch barriers 创建一个读者写者锁 提供了一个优雅的解决方案。
在访问数据库或文件时, 为了提高效率, 读取操作放在并行队列中执行. 但是写入操作必须在串行队列中执行(避免资源抢夺问题). 为了避免麻烦, 此时dispatch_barrier_async函数作用就出来了, 在这函数里进行写入操作, 写入操作会等到所有读取操作完毕后, 形成一道栅栏, 然后进行写入操作, 写入完毕后再把栅栏移除, 同时开放读取操作.

快速迭代

dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ // code here });

执行10次代码, index顺序不确定. dispatch_apply会等待全部处理执行结束才会返回. 意味着dispatch_apply会阻塞当前线程. 所以dispatch_apply一般用于异步函数的block中.

那何时才适合用 dispatch_apply 呢?

自定义串行队列:串行队列会完全抵消 dispatch_apply 的功能;你还不如直接使用普通的 for 循环。
主队列(串行):与上面一样,在串行队列上不适合使用 dispatch_apply 。还是用普通的 for 循环吧。
并发队列:对于并发循环来说是很好选择,特别是当你需要追踪任务的进度时。

一次性代码
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });
该代码在整个程序的生命周期中只会执行一次.

挂起和恢复
dispatch_suspend(queue)
挂起指定的queue队列, 对已经执行的没有影响, 追加到队列中尚未执行的停止执行.

dispatch_resume(queue)
恢复指定的queue队列, 使尚未执行的处理继续执行.

GCD的注意点
因为在ARC下, 不需要我们释放自己创建的队列, 所以GCD的注意点就剩下死锁
死锁

1
2
3
4
5
NSLog(@"111");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"222");
});
NSLog(@"333");

以上三行代码将输出什么?
111
222
333 ?
还是
111
333 ?
其实都不对, 输出结果是
111
无疑问会先输出111, 然后在当前队列下调用dispatch_sync函数, dispatch_sync函数会把block追加到当前队列上, 然后等待block调用完毕该函数才会返回, 不巧的是, block在队列的尾端, 而队列正在执行的是dispatch_sync函数. 现在的情况是, block不执行完毕, dispatch_sync函数就不能返回, dispatch_sync不返回, 就没机会执行block函数. 这种你等我, 我也等你的情况就是死锁, 后果就是大家都执行不了, 当前线程卡死在这里.

如何避免死锁?
不要在当前队列使用同步函数, 在队列嵌套的情况下也不允许. 如下图,

队列嵌套调用同步函数引发死锁

大家可以想象, 队列1执行完NSLog后到队列2中执行NSLog, 队列2执行完后又跳回队列1中执行NSLog, 由于都是同步函数, 所以最内层的NSLog(“333”); 追加到队列1中, 实际上最外层的dispatch_sync是还没返回的, 所以它没有执行的机会. 也形成死锁. 运行程序, 果不其然, 打印如下 :
111
222

GCD实现.(GCD的使用场景)

线程间的通信

这是GCD最常用的使用场景了, 如下代码

1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程作刷新UI等操作
});
});

为了不阻塞主线程, 我们总是在后台线程中发送网络请求, 处理数据, 然后再回到主线程中刷新UI界面.

单例

单例也就是在程序的整个生命周期中, 该类有且仅有一个实例对象, 此时为了保证只有一个实例对象, 我们这里用到了dispatch_once函数

同步队列和锁
利用队列, 实现getter方法可以并发执行, 而setter方法串行执行并且setter和getter不能并发执行呢??? 没错, 我们这里用到了dispatch_barrier_async函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSString )myString
{
__block NSString localMyString = nil;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
localMyString = self.myString;
});
return localMyString;
}
- (void)setMyString:(NSString *)myString
{
dispatch_barrier_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_myString = myString;
});
}

这里利用了栅栏块必须单独执行, 不能与其他块并行的特性, 写入操作就必须等当前的读取操作都执行完毕, 然后单独执行写入操作, 等待写入操作执行完毕后再继续处理读取.

Dispatch Source
它是BSD系内核惯有功能kqueue的包装. kqueue的CPU负荷非常小, 可以说是应用程序处理XNU内核中发生的各种事件的方法中最优秀的一种.

面试题

  1. 介绍一下GCD

  2. 如果没有GCD,你怎样实现多线程

  3. NSthread的缺点是什么?使用NSthread怎么实现数据同步?

  4. 如何防止死锁;

  5. GCD如何实现同步任务,即如何执行完一段代码后再去执行另一段代码;(线程组,barrier,信号量)

  6. iOS中如何实现单例,用GCD来实现一下;

  7. GCD中如何创建异步线程,GCD中是否能stop一个线程执行;
  8. GCD中有哪些方法?

  9. GCD和NSOperation的区别;哪一个的复用性更好;NSOperation的队列可以cancel吗,里面的任务可以cancel吗; NSOperation并发有顺序吗?

  1. 多线程有哪几种实现方式,GCD的具体使用;

  2. GCD中的数据不安全是怎么处理的;

  3. dispatch_main,同步和异步如何使用;

  4. GCD指向了野指针了怎么办

  5. GCD有何缺点?

  1. [※※※※]如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
    使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
1
2
3
4
5
6
7
8
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 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
  1. [※※※※]dispatch_barrier_async的作用是什么?
  2. [※※※※※]苹果为什么要废弃dispatch_get_current_queue?

参考:
Objective-C高级编程书。
https://github.com/nixzhu/dev-blog/blob/master/2014-04-19-grand-central-dispatch-in-depth-part-1.md

文章目录
  1. 1. GCD
    1. 1.1. GCD概要
    2. 1.2. GCD的API
    3. 1.3. GCD实现.(GCD的使用场景)
  • 面试题
  • 本站总访问量 本站访客数人次 ,