前言
多线程可以做什么?
- 网络请求
- 文件 IO
- 复杂计算
- 数据模型转换...
Swift 中的多线程编程方式:
- Thread
- Cocoa Operation(Operation 和 OperationQueue)
- Grand Central Dispatch (GCD)
1. Thread 创建与使用
Thread 的快捷创建方式:
-
detachNewThread(_ block: @escaping() -> Void)func threadTest() { for i in 0..<10 { Thread.detachNewThread { print(i) } } } // 打印结果:0 1 2 4 5 6 7 8 9 3(乱序的) -
detachNewThreadSelector(_ selector: Selector, toTarget target:Any, with argument: Any?)Thread.detachNewThreadSelector(#selector(threadSelectorTest), toTarget: self, with: nil) @objc func threadSelectorTest() { print("thread Selector Test") }
Thread 的初始化器:
使用Thread(target:, selector:, object)来初始化一个 Thread,然后调用 start() 函数启动线程。
class ObjectForThread {
func threadTest() {
let thread = Thread(target: self, selector: #selector(threadWorker), object: nil)
thread.start()
}
@objc func threadWorker() {
print("threadWorker")
}
}
let t = ObjectForThread()
t.threadTest()
2. Operation 和 OperationQueue
Operation 是面向对象的多线程技术,需要配合 OperationQueue 一起使用,Operation 可以取消、设置依赖关系。可以完成任务优先级的其他复杂逻辑,当然也可以子类化。
2.1 Operation 简介
在 Swift 中 Operation 有两种:
-
Operation
class MyOperation: Operation { override func main() { sleep(1) print("this is my operation") } } let operation = MyOperation() let queue = OperationQueue() queue.addOperation(operation) print("operationTest") // operationTest // this is my operation -
BlockOperation
let operation = BlockOperation { [weak self] in self?.threadWorker() return } func threadWorker() { print("threadWorker") } let queue = OperationQueue() queue.addOperation(operation) print("operationTest") // operationTest // threadWorker
Operation 的状态:
- isReady: 是否准备好了可以执行
- isExecuting: 是否正在执行中
- isFinished: 是否完成了
- isCancelled: 是否取消了
Operation 有两种执行方式:同步(sync)和异步(async),同步的话需要重写其 main() 方法,异步的话需要重写其 start() 方法, 并且注意 isAsynchronous、isExecuting、isFinished、isCancelled 的状态。
Operation 还有一个完成的回调为:completionBlock, 其实对于同步执行的 Operation 来说,completionBlock 存在的意义不大,我们完全可以在重写 main() 函数的函数体里面等执行完我们的任务的时候再通知外界执行完毕,但是这个回调对于异步执行的 Operation 很重要,可以在这个回掉中获得执行完毕的通知。
class MyOperation: Operation {
override func main() {
sleep(1)
print("this is my operation")
}
}
let operation = MyOperation()
operation.completionBlock = { () -> Void in
print("completionBlock")
}
let queue = OperationQueue()
queue.addOperation(operation)
print("operationTest")
// operationTest
// this is my operation
// completionBlock
2.2 OperationQueue 简介
- OperationQueue 队列里可以加入很多个 Operation,可以把 OperationQueue 看作一个线程池,可往线程池中添加操作(Operation)到队列中。
- 底层使用的是 GCD。
- maxConcurrentOperationCount 可以设置最大并发数。
- defaultMaxConcurrentOperationCount 根据当前系统条件确定的最大并发数。
- 可以取消所有 Operation,但是当前正在执行的不会取消
- 当所有的 Operation 执行完毕后 OperationQueue 会退出并销毁。
3. GCD
3.1 GCD 简介
GCD 的特点:
- 任务 + 队列
- 易用性
- 效率高
- 性能好
GCD 的队列:
- 主队列:任务在主线程执行
- 并行队列:任务会以先进先出的顺序入列和出列,但是因为同一时刻多个任务并发执行,所有任务的执行顺序不一定。
- 串行队列:任务会以先进先出的顺序入列和出列,但是同一时刻只会执行一个任务。
GCD 的队列API:
- Dispatch.main: 主队列
- Dispatch.global: 全局队列
- DispatchQueue(label:,qos:,attributes:,autoreleaseFrequency:,target:): 队列的初始化器
- queue.label: 队列的标签,调试时使用
- setTarget(queue: DispatchQueue?): 设置目标队列
补充说明:
DispatchQoS: 全局并发队优先级
老版(swift3之前)gcd有四种:
high,default,low,background, 优先级为:high>default>low>background新版(swift3之后)gcd有六种:
background,utility,default,userInitiated,userInteractive,unspecified优先级为:userInteractive>default>unspecified>userInitiated>utility>background
DispatchQueue.Attributes 队列的选项集合(option sets),包含两个选项:
- concurrent:标识队列为并行队列。
- initiallyInactive:标识运行队列中的任务需要手动触发,由队列的 activate 方法进行触发。如果未添加此标识,向队列中添加的任务会自动运行。
如果不设置该值,则表示创建串行队列。如果希望创建并行队列,并且需要手动触发,则该值需要设置为 [.concurrent, .initiallyInactive]
DispatchQueue.AutoreleaseFrequency: autoreleaseFrequency 的类型为枚举(enum),用来设置负责管理任务内对象生命周期的 autorelease pool 的自动释放频率。包含三个类型:
- inherit:继承目标队列的该属性,
- workItem:跟随每个任务的执行周期进行自动创建和释放
- never:不会自动创建 autorelease pool,需要手动管理。
一般采用 workItem 行了。如果任务内需要大量重复的创建对象,可以使用 never 类型,来手动创建 aotorelease pool。
target: 这个参数设置了队列的目标队列,即队列中的任务运行时实际所在的队列。目标队列最终约束了队列的优先级等属性。 在程序中手动创建的队列最后都指向了系统自带的
主队列或全局并发队列。
3.2 GCD 基本操作
-
sync: 同步执行,提交任务到当前队列里,并且直到任务执行完成,当前队列才会返回。
let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) // 同步执行 queue.sync { print("in queue sync") } print("after invoke queue sync method") // 执行结果: // in queue sync // after invoke queue sync method -
async: 异步执行,调度一个任务去立即执行,但是不用等任务执行完毕当前队列就会返回。
let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) // 异步执行 queue.async { sleep(2) print("in queue async") } print("after invoke queue method") // 执行结果: // after invoke queue method // in queue async -
asyncAfter: 延时异步执行,调度一个任务多久去执行,但是不用等任务执行完毕当前队列就会返回。
let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) // 延时 2 秒执行 queue.asyncAfter(deadline: .now() + 2) { print("in queue asyncAfter") } print("after invoke queue method") // 执行结果: // after invoke queue method // in queue asyncAfter
3.3 GCD 高级特性
3.3.1 DispatchGroup
DispatchGroup 的 wait 使用示例:
let workQueue = DispatchQueue.global()
let workGroup = DispatchGroup()
workGroup.enter()
workQueue.async {
Thread.sleep(forTimeInterval: 1)
print("请求 A 接口数据完成")
workGroup.leave()
}
workGroup.enter()
workQueue.async {
Thread.sleep(forTimeInterval: 2)
print("请求 B 接口数据完成")
workGroup.leave()
}
print("我是最开始执行的, 异步打印后续执行")
workGroup.wait()
print("整合接口 A 和 接口 B 的数据, 刷新页面!")
// 执行结果:
// 我是最开始执行的, 异步打印后续执行
// 请求 A 接口数据完成
// 请求 B 接口数据完成
// 整合接口 A 和 接口 B 的数据, 刷新页面!
DispatchGroup 的 notify 使用示例:
let workQueue = DispatchQueue.global()
let workGroup = DispatchGroup()
workGroup.enter()
workQueue.async {
Thread.sleep(forTimeInterval: 1)
print("请求 A 接口数据完成")
workGroup.leave()
}
workGroup.enter()
workQueue.async {
Thread.sleep(forTimeInterval: 2)
print("请求 B 接口数据完成")
workGroup.leave()
}
print("我是最开始执行的, 异步打印后续执行")
workGroup.notify(queue: workQueue) {
print("整合接口 A 和 接口 B 的数据, 刷新页面!")
}
print("验证不堵塞线程")
// 执行结果:
// 我是最开始执行的, 异步打印后续执行
// 验证不堵塞线程
// 请求 A 接口数据完成
// 请求 B 接口数据完成
// 整合接口 A 和 接口 B 的数据, 刷新页面!
3.3.2 DispatchSource
简单来说,DispatchSource 是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个 task 放入一个 DispatchQueue 的执行例程中。
DispatchSource 的类型有以下几种:
- Mach port send right state changes
- Mach port receive right state changes
- External process state change
- file descriptor ready for read
- file descriptor ready for write
- Filesystem node event
- POSIX signal
- Custom timer
- Custom event
下面以 timer 举例说明如何使用:
var seconds = 5
let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating: 1)
timer.setEventHandler {
seconds -= 1
if seconds < 0 {
timer.cancel()
} else {
print(seconds)
}
}
timer.resume()
// 执行结果:
// 4 3 2 1 0
4. 多线程应用场景
- 一个页面有三个网络请求,需要在三个网络请求都返回数据的时候刷新页面。(思路:使用 DispatchGroup)
- 实现一个线程安全的 Array 的读和写。(思路:加锁,使用并发队列完成读写操作)
- 编写一个多线程下载器,可以执行多个下载任务,每个任务可以保存当前下载字节数,总字节数,可以设置回调得到当前的下载进度。(思路: 使用 Operation 和 OperationQueue,子类化 Operation 来完成自己的内部业务逻辑)
- 需要在主线程等待一个异步任务返回,才能继续执行下面的逻辑,但是又不希望堵塞用户交互事件。(思路:使用并发队列异步执行任务,完毕后通知主队列刷新 UI)
5. 多线程编程模式
- Promise
- Pipeline
- Master-Slave
- Serial Thread Confinement
5.1 Promise
所谓的 Promise 就是一个对象用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可以进一步处理。
Promise 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称为 Incomplete)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 或 从 Pending 变为 Rejected。只要这两种情况之一发生了,状态就凝固了,不会再改变了,会一直保持这个结果
普通的编程模式:
Promise 的编程模式:
5.2 Pipeline
- Pipeline 是将一个任务分解为若干个阶段(Stage),前阶段的输出作为下阶段的输入,各个阶段有不同的工作者线程负责执行。
- 多个任务的各个阶段是并行(Parallel)处理的。
- 具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务来看,不同任务的各个阶段的执行是并行的。
Pipeline 的工作流程:
5.3 Master-Slave
Master-Slave 是将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既提高了计算效率,又实现了信息隐藏。
Master-Slave 的工作流程:
5.4 Serial Thread Confinement
如果并发任务的执行涉及某个非线程安全的对象,而很多时候我们又不希望因此而引入锁(担心死锁、优先级翻转或其他不同线程调度资源而出现问题)。这时候可以通过将多个并发的任务存入队列实现任务的串行化,并未这些串行化的任务创建为宜的工作者线程进行处理。