简介
为何使用GCD
GCD提供很多超越传统多线程编程的优势:
易用
: GCD比之thread跟简单易用。
由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。
基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文。效率
: GCD被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。
这关系到易用性:导致GCD易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。性能
: GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。
GCD编程的核心就是dispatch队列
,block
的执行最终都会放进某个队列中去进行,它类似NSOperationQueue
但更复杂也更强大,并且可以嵌套使用。所以说,结合block
实现的GCD
,把函数闭包(Closure)
的特性发挥得淋漓尽致
总而言之 就是dispatch队列
执行
block
block就不用说了
下面就说一下dispatch队列
的创建 和 如何执行block
队列的创建
dispatch队列的生成有三种方式
OC
dispatch_get_main_queue()
dispatch_get_global_queue(long identifier, unsigned long flags)
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
Swift3.0
DispatchQueue.main
DispatchQueue.global(qos: .userInitiated)
DispatchQueue(label: "myBackgroundQueue")
这三种方式又可以分为两大类
- 在
主线程
执行block的dispatch_get_main_queue
- 在
其它线程
执行block的dispatch_get_global_queue
和dispatch_queue_create
也可以分为串行
和并行
两大类
下面就详细介绍一下各方法
1) 主线程队列
OC
1 | dispatch_queue_t mainQueue = dispatch_get_main_queue(); |
Swift3.0
1 | DispatchQueue.main.async { [weak self] in |
获得主线程
的dispatch队列,它没有参数
,实际是一个串行队列
。无法控制
主线程dispatch队列的执行继续或中断
。
2) 全局队列
OC
1 | dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); |
Swift3.0
1 | DispatchQueue.global(qos: .userInitiated).async { |
参数对应
1 | DISPATCH_QUEUE_PRIORITY_HIGH: .userInitiated |
- 获得程序进程缺省产生的
并行队列
,可设定优先级来选择高
>默认
>低
>后台
四个个优先级队列。 - 它的第二个参数是系统预留的,现在还没用,以后有用时 值是非0的,所以现在都设置为0
- 由于是系统默认生成的,所以
无法调用`
dispatch_resume()和
dispatch_suspend()`来控制执行继续或中断。 - 需要注意的是,三个队列不代表三个线程,可能会有更多的线程。并发队列可以根据实际情况来自动产生合理的线程数,也可理解为dispatch队列实现了一个线程池的管理,对于程序逻辑是透明的。
- 也就是说它是内置的一个比较NB的
并行队列
,能安装优先级来处理并行block
3) 自定义队列
OC
1 | dispatch_queue_t mySerialQueue = dispatch_queue_create("cn.psvmc.task1", DISPATCH_QUEUE_SERIAL);//串行 |
Swift3.0
最简单直接的办法是这样:
1 | let queue = DispatchQueue(label: "myBackgroundQueue") |
可以指定优先级以及队列类别:
1 | let queue = DispatchQueue(label: "myBackgroundQueue", qos: .userInitiated, attributes: .concurrent) |
第一个参数是队列的名称,不能重复
第二个参数来指定队列是串行(DISPATCH_QUEUE_SERIAL)
还是并行(DISPATCH_QUEUE_CONCURRENT)
设置为nil时 为串行
当队列为串行时 队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行
当队列为并行时,没有固定的顺序,为多线程执行
队列执行
基本
dispatch_async
和 dispatch_sync
1 | //异步执行block,函数立即返回 |
实际编程经验告诉我们,尽可能避免使用dispatch_sync
,嵌套使用时还容易引起程序死锁
。
死锁例子
- 死锁例子1
如果queue1
是一个串行队列
的话,这段代码立即产生死锁:
1 | dispatch_sync(queue1, ^{ |
- 死锁例子2
主线程中执行会死锁
1 | dispatch_sync(dispatch_get_main_queue(), ^{ |
实际应用
那实际运用中,一般可以用dispatch
这样来写,常见的网络请求数据多线程执行模型:
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
多线程操作数据库
dispatch
队列是线程安全的,可以利用串行队列实现锁的功能。比如多线程写同一数据库,需要保持写入的顺序和每次写入的完整性,简单地利用串行队列即可实现:
1 | dispatch_queue_t queue1 = dispatch_queue_create("com.dispatch.writedb", DISPATCH_QUEUE_SERIAL); |
下一次调用writeDB:
必须等到上次调用完成后才能进行,保证writeDB:
方法是线程安全的。
重复执行
dispatch_apply
1 | //重复执行block,需要注意的是这个方法是同步返回,也就是说等到所有block执行完毕才返回,如需异步返回则嵌套在dispatch_async中来使用。 |
随后执行
dispatch_barrier_async
和 dispatch_barrier_sync
1 | //这个函数可以设置同步执行的block,它会等到在它加入队列之前的block执行完毕后,才开始执行。 |
只执行一次
dispatch_once
1 | static dispatch_once_t onceToken; |
上面的代码只会执行一次 打印
1 | dispatch_once:0 |
延迟执行
dispatch_after
1 | //延迟执行block |
例子
OC
1 | // 延迟2秒执行: |
Swift3.0
1 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3.0) { |
调度执行
dispatch_set_target_queue
dispatch队列的一个很有特色的函数:
1 | void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue); |
它会把需要执行的任务对象指定到不同的队列中去处理,这个任务对象可以是dispatch队列
,也可以是dispatch源
。而且这个过程可以是动态的,可以实现队列的动态调度管理等等。
比如说有两个队列dispatchA
和dispatchB
,这时把dispatchA
指派到dispatchB
:
1 | dispatch_set_target_queue(dispatchA, dispatchB); |
那么dispatchA
上还未运行的block
会在dispatchB
上运行。
这时如果暂停dispatchA
运行:
1 | dispatch_suspend(dispatchA); |
则只会暂停dispatchA
上原来的block
的执行,dispatchB
的block
则不受影响。
而如果暂停dispatchB
的运行,则会暂停dispatchA
的运行。
这里只简单举个例子,说明dispatch队列运行的灵活性,在实际应用中你会逐步发掘出它的潜力。
dispatch队列
不支持cancel(取消),没有实现dispatch_cancel()
函数,不像NSOperationQueue
,不得不说这是个小小的缺憾。
分组执行
我们的应用不是简单的同步也异步的运行,应用经常是混合的
比如我们要task1
task2
都运行完成后才能异步运行task3
task4
我们该怎么做呢?这里我们可以引入group
的概念
1 | dispatch_queue_t dafaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
有时候我们也可以为queue定义一个结束函数
dispatch_set_finalizer_f
是在dispatch_release
时候被调用
1 | dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL); |
Swift
对于组,现在你可以使用这样的语法直接创建一个组:
1 | let group = DispatchGroup() |
至于使用,则是这样的:
1 | let group = DispatchGroup() |
那么,如果有多个并发队列在同一个组里,我们需要它们完成了再继续呢?
1 | group.wait() |
不再使用锁(Lock)
用户队列可以用于替代锁来完成同步机制。在传统多线程编程中,你可能有一个对象要被多个线程使用,你需要一个锁来保护这个对象:
1 | NSLock *lock; |
访问代码会像这样:
1 | - (id)something { |
使用GCD,可以使用queue来替代:
1 | dispatch_queue_t queue; |
要用于同步机制,queue必须是一个用户队列,而非全局队列,所以使用dispatch_queue_create
初始化一个。
然后可以用dispatch_async
或者 dispatch_sync
将共享数据的访问代码封装起来:
1 | - (id)something { |
值得注意的是dispatch_queue
是非常轻量级的,所以你可以大用特用,就像你以前使用lock
一样。
现在你可能要问:“这样很好,但是有意思吗?我就是换了点代码办到了同一件事儿。”
实际上,使用GCD途径有几个好处:
- 平行计算: 注意在第二个版本的代码中,
-setSomething:
是怎么使用dispatch_async
的。调用-setSomething:
会立即返回,然后这一大堆工作会在后台执行。如果updateSomethingCaches
是一个很费时费力的任务,且调用者将要进行一项处理器高负荷任务,那么这样做会很棒。 - 安全: 使用GCD,我们就不可能意外写出具有不成对Lock的代码。在常规Lock代码中,我们很可能在解锁之前让代码返回了。使用GCD,队列通常持续运行,你必将归还控制权。
- 控制: 使用GCD我们可以挂起和恢复
dispatch_queue
,而这是基于锁的方法所不能实现的。我们还可以将一个用户队列指向另一个dspatch_queue
,使得这个用户队列继承那个dspatch_queue
的属性。使用这种方法,队列的优先级可以被调整——通过将该队列指向一个不同的全局队列,若有必要的话,这个队列甚至可以被用来在主线程上执行代码。 - 集成: GCD的事件系统与
dspatch_queue
相集成。对象需要使用的任何事件或者计时器都可以从该对象的队列中指向,使得这些句柄可以自动在该队列上执行,从而使得句柄可以与对象自动同步。