最近几天陆陆续续把 raywenderlich 出品的《Concurrency by tutorials》 看完了,写篇文章总结一下。
Q1:什么是并发?并发和并行的区别?
如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于“存在”这个词。
在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。
我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
Q2:什么是 GCD(Grand Central Dispatch)?
GCD 是 Apple 对 C 语言 libdispatch 库的实现。它的目标是使需要执行的任务入队列,然后以某种方式执行队列中的任务。GCD 在实现中使用线程,所以开发者就无需亲自管理线程,亲自创建销毁线程是比较复杂的事情。
Q3:什么是串行队列?什么是并发队列?
- 串行队列和并发队列都是先进先出(FIFO),区别在于其队列中任务的执行方式。
- 串行队列中,下一个任务会等待上一个任务结束后才会执行。
- 并发队列中,不会等待上一个任务完全执行结束,就会立即调用执行下一个任务。
需要注意的是:这里的 FIFO 是以任务开始的时间为基准的,而不是任务完成的时间。
队列中任务的执行方式是与使用同步函数还是异步函数相关。
负责更新 UI 的 main queue 就是一个串行队列。
Q4:什么是同步任务?什么是异步任务?
被塞入队列里的任务要么以同步的方式运行,要么以异步的方式运行。
当任务以同步的方式运行时,当前队列会被阻塞,直到该任务执行完毕,才会执行之后的代码。
当任务以异步的方式运行时,任务入队列,当前队列不会被阻塞,继续执行代码。
Q5:GCD,Operation,BlockOperation 三者该怎么选择?
Operation 是建立于 GCD 之上的。GCD 往往适用于在后台运行的轻量任务。当遇到需要执行重复次数多且耗时的任务时,比如修改图片景深等,建议选择 Operation。Operation 默认是同步执行的,如果想要异步执行 Operation,还需要更多的工作,可以参考 官方文档 中 Subclassing Notes 一节。Operation 还提供给我们跟踪任务运行状态以及取消任务的功能。
介于两种情况之间呢,BlockOperation 是个好选择,但要注意的是 BlockOperation 默认是并发执行的。如果想要 BlockOperation 串行执行,我们得额外建立一个 dispatch queue。
Q6:该如何选择六种不同的 QoS?
当使用并发队列时,Apple 提供六种不同的全局并发队列,即有六种不同的 QoS。不同的 QoS 代表队列中任务不同的优先级。
.userInteractive
对应的任务一般都是 UI 更新以及动画效果相关的。提交到这类队列的任务应该是瞬时完成的,不然可能会有些卡顿出现。
.userInitiated
对应的任务一般用户从 UI 触发的可以异步执行的任务,比如点击一个 button 打开一个 pdf 文档。该队列中的任务应该在几秒或更少的时间内完成。
.utility
对应的任务往往是较长时间的网络相关的任务。这种任务往往需要在 UI 上显示一个 HUD。该队列中的任务应该在几秒到几分钟的时间内完成。
.background
对应的任务往往是用户无法直接感知到的任务,比如与远端服务器同步,执行备份等。这类任务往往不需要人机交互且对运行时间的长短不太敏感。
.default and .unspecified
几乎很少会去主动使用这两种 QoS,知道它俩的存在就行。
Q7:什么时候使用 DispatchWorkItem?
使用 DispatchWorkItem 的原因之一可能是我们需要在这个任务运行前或运行中取消它。
对这个 DispatchWorkItem 调用 cancel() 函数,可能会出现以下两种情况:
- 如果这个任务还未开始执行,则这个任务会被移除队列。
- 如果这个任务正在执行,它的 isCancelled 属性会被设置为 true。
多个 DispatchWorkItem 之间还可以添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let backgroundWorkItem = DispatchWorkItem { print("backgroundWorkItem start!") Thread.sleep(forTimeInterval: 5) print("backgroundWorkItem finish!") } let updateUIWorkItem = DispatchWorkItem { print("update UI start") Thread.sleep(forTimeInterval: 2) print("update UI finish") } backgroundWorkItem.notify(queue: .main, execute: updateUIWorkItem) dispatchQueue.async(execute: backgroundWorkItem)
|
但是,如果觉得需要取消任务的执行或者指明任务间的依赖,还是建议使用 Operation。
Q8:什么时候使用 Dispatch Group?
有这么一些任务,我们需要知道它们何时都完成了,在这种情况下,建议使用 Dispatch Group。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| let group = DispatchGroup() let queue = DispatchQueue.global(qos: .userInitiated)
queue.async(group: group) { print("Start job 1") Thread.sleep(until: Date().addingTimeInterval(10)) print("End job 1") }
queue.async(group: group) { print("Start job 2") Thread.sleep(until: Date().addingTimeInterval(2)) print("End job 2") }
if group.wait(timeout: .now() + 20) == .timedOut { print("I got tired of waiting") } else { print("All the jobs have completed") }
group.notify(queue: .main) { print("all task finished!") }
|
网络请求回调往往是异步的,当 group 中的任务是异步任务时,我们可以利用 enter() 和 leave() 来表示任务进入 group 和在 group 中的任务已经完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let group = DispatchGroup() group.enter() // 网络请求回调函数1里调用 leave() requestForXXX().finally { group.leave() } group.enter() // 网络请求回调函数2里调用 leave() requestForXXX().finally { group.leave() } group.enter() // 网络请求回调函数3里调用 leave() requestForXXX().finally { group.leave() } group.notify(queue: .main) { // 三个网络请求均完成后,需要执行的操作 // ... }
|
这里需要注意的一点是: enter() 和 leave() 调用的次数要相匹配。
Q9:什么时候使用信号量(Semaphores)?
当需要控制访问共享资源的线程在某个范围内时,或者举个更具体的例子:当从网络上下载数据时,可以利用信号量来控制同时最多多少个下载任务进行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let group = DispatchGroup() let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
for i in 1...10 { semaphore.wait() queue.async(group: group) { defer { semaphore.signal() } print("Downloading image \(i)") // Simulate a network wait Thread.sleep(forTimeInterval: 3) print("---Downloaded image \(i)") } }
group.wait()
|
Q10:什么时候使用 Thread barrier ?
当面对多读单写的时候,手动加锁可能是比较复杂的事情,我们可以使用 GCD 提供的 thread barrier。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent) private var _count = 0 public var count: Int { get { return threadSafeCountQueue.sync { return _count } } set { threadSafeCountQueue.async(flags: .barrier) { [weak self] in guard let self = self else { return } self._count = newValue } } }
|
Q11: 如何写一个异步的 Operation?
- 继承父类 Operation
- 手动管理 Operation 的状态
- 根据 官方文档,覆写(override)指定的父类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class AsyncOperation: Operation { enum State: String { case ready, executing, finished
fileprivate var keyPath: String { return "is\(rawValue.capitalized)" } }
// 手动触发 KVO var state = State.ready { willSet { willChangeValue(forKey: newValue.keyPath) willChangeValue(forKey: state.keyPath) } didSet { didChangeValue(forKey: oldValue.keyPath) didChangeValue(forKey: state.keyPath) } }
// Override properties override var isReady: Bool { return super.isReady && state == .ready }
override var isExecuting: Bool { return state == .executing }
override var isFinished: Bool { return state == .finished }
override var isAsynchronous: Bool { return true }
// 无需调用 super.start() override func start() { if isCancelled { state = .finished // 手动管理状态 return }
main() state = .executing // 手动管理状态 } }
|
AsyncOperation 严格算是个抽象类。当具体实现时,记得继承 AsyncOperation 。举个例子,把从网络下载图片的请求封装成一个 Operation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| final class NetworkImageOperation: AsyncOperation { var image: UIImage?
private let url: URL private let completion: ImageOperationCompletion ... ... ... // 覆写 main() ,在 main() 中写所需执行的代码 override func main() { URLSession.shared.dataTask(with: url) { [weak self] data, response, error in guard let self = self else { return }
defer { self.state = .finished } // 手动管理 Operation 状态
if let completion = self.completion { completion(data, response, error) return }
guard error == nil, let data = data else { return }
self.image = UIImage(data: data) }.resume() //千万别他妈再忘了!!! } }
|
Q12: 什么时候使用 Operation 依赖?
当多个任务之间有先后次序时,我们可以选择使用 Operation 依赖,而 GCD 却不是个好选择。
1 2 3 4 5 6 7 8 9
| let downloadOp = NetworkImageOperation(url: urls[indexPath.row]) let tiltShiftOp = TiltShiftOperation() tiltShiftOp.addDependency(downloadOp)
... ...
queue.addOperation(downloadOp) queue.addOperation(tiltShiftOp)
|
但没有处理好任务之间的依赖,是有可能导致死锁。
Q13: Thread sanitizer 有什么用?
Thread sanitizer 可以帮助开发者发现多线程编程中的竞态条件(race condition),简称 TSan。让我来举一个 race condition 的例子:首先定义变量 var count = 0
,然后并发执行 count += 1
和 count -= 1
就可能出现 race condition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // count += 1 背后的实现可以理解为: register1 = count register1 = register1 + 1 count = register1
// count -= 1 背后的实现可以理解为: register2 = count register2 = register2 + 1 count = register2
// 所以并发执行 count += 1 和 count -= 1 时,底层的代码执行顺序可能是任意的 T0: 执行 register1 = count // register1 = 5 T1: 执行 register1 = register1 + 1 // register1 = 6 T2: 执行 register2 = count // register2 = 5 T3: 执行 register2 = register2 - 1 // register2 = 4 T4: 执行 count = register1 // count = 6 T5: 执行 count = register2 // count = 4
// 两个进程并发操作 count,且运行结果依赖于操作的先后次序,这种情况称为 race condition。
|
再继续看一下更具体的例子:
首先先开启 Thread Sanitizer。
运行以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| final class MainViewController: UIViewController { var counter = 0 override func viewDidLoad() { super.viewDidLoad() let queue = DispatchQueue(label: "q") queue.async { for _ in 1 ... 10000 { Thread.sleep(forTimeInterval: 0.1) // 强制切换线程 self.counter += 1 } }
DispatchQueue.main.async { for _ in 1 ... 10000 { self.counter += 1 } } } }
|
Race condition 发生了:
需要注意的是:TSan 仅支持在模拟器下运行。
结语
读完后,为 iOS 多线程方面打下了一点点基础,还是要在工作中多实践才有更深的感悟。