Swift多线程

本文介绍三种不同的多线程技术,分别是Thread、Grand Central Dispatch(GCD)和Operation。

Thread

Thread在三种多线程技术中是最轻量级的,但需要自己来管理线程的生命周期和同步问题。Thread有两种创建方式:

  • 通过工厂方法直接创建线程并自动运行
  • 先创建一个线程对象,然后手动运行线程
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
class ThreadTest {
// 1、使用类方法
func run1() {
print("Thread1 start")
Thread.detachNewThreadSelector(#selector(longRunningTask), toTarget: self, with: nil)
}

// 2、使用构造器
func run2() {
print("Thread2 start")
let thread = Thread(target: self, selector: #selector(longRunningTask), object: nil)
// 需要手动启动
thread.start()
}

@objc func longRunningTask() {
for i in 0..<5 {
print(i)
}
}
}

let threadTest = ThreadTest()
threadTest.run1()
threadTest.run2()

Grand Central Dispatch(GCD)

GCD的基本概念是将任务添加到调度队列中,队列中的任务稍后会以先到先执行的顺序执行。任务分为同步任务和异步任务,同步任务会阻塞当前线程,直到任务完成。异步任务不会等待任务执行结束,而是马上返回到当前线程继续执行下一个任务。队列也分为串行队列和并发队列,串行队列在任何时刻都只有一个任务被执行,并发队列可能会有多个任务并发执行,这依赖于系统环境。

队列

使用DispatchQueue来新建调度队列,新建后系统会为该队列分配线程。

1
2
3
4
5
6
// 使用独一无二的label参数来表示队列意图
// 默认创建的是串行队列
let queue = DispatchQueue(label: "com.gcfeng.networking")

// 如果要创建并发队列,可以传递attributes参数
let queue = DispatchQueue(label: "com.gcfeng.networking", attributes: .concurrent)
应用启动后,程序会默认创建一个主调度队列(main dispatch queue),主调度队列是一个串行队列,负责UI渲染,通过DispatchQueue.main可以访问该主队列。需要注意的是不要在主队列中执行同步任务,除非这些任务是更新UI的,否则可能会造成界面卡顿。

任务有优先级,优先级高的会先执行,分配有更多资源,但是也希望执行时间尽可能快。系统使用QoS(Quality of Service)来描述优先级,共定义了6种不同的QoS:

  • .userInteractive:用户交互的任务,比如动画或任何希望UI快速更新的任务。优先级最高
  • .userInitiated:需要马上获取结果,比如打开一个文档或者从本地数据库中读取记录
  • .utility:可以执行很长时间,然后通知用户结果。比如文件下载,给用户下载进度
  • .background:耗时较久的任务,且用户并不关心完成时间。比如后台数据同步等
  • .default和.unspecified:.default是默认值,介于.userInitiated和.userInteractive之间。.unspecified是为了兼容旧API的。这两个值都不推荐直接使用。
1
2
3
4
5
// 在创建队列的时候,可以传递qos参数来指定队列的优先级
let queue = DispatchQueue(label: "com.gcfeng.doc", qos: .userInitiated, attributes: .concurrent)

// 如果不希望自己管理,可以获取系统预定义的全局并发队列:
let queue = DispatchQueue.global(qos: .userInitiated)

任务

任务是一段闭包代码。队列使用sync来创建同步任务,使用async来创建异步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建了异步任务
DispatchQueue.global(qos: .utility).async { [weak self] in
// [weak self]表示我们弱引用self变量,那么在使用self变量之前需要进行检查
// 实际上GCD的异步任务不会造成循环引用,所以这里是否使用弱引用因需求而定
guard let self = self else { return }

// 执行任务代码
// ...

// 回到主线程来更新UI
DispatchQueue.main.async {
self.textLabel.text = "New post available!"
}
}

可以将队列中的任务分组,当组内所有任务都完成时发出通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GCDTest {
func run() {
// 新建一个队列
let queue = DispatchQueue(label: "com.gcfeng.test", attributes: .concurrent)
// 新建一个组
let group = DispatchGroup()
queue.async(group: group) {
for i in 0..<5 {
print("task1: \(i)")
}
}
queue.async(group: group) {
for i in 0..<5 {
print("task2: \(i)")
}
}
// 当组内的任务全部完成时,通知主线程
group.notify(queue: DispatchQueue.main) {
print("All task finish")
}
}
}
let gcdTest = GCDTest()
gcdTest.run()
如果任务代码中包含异步方法,任务不会等待里面的异步方法执行完毕就返回了,而队列却又认为任务已经执行完毕,这样就出现问题了。组提供了enter和leave方法来解决这个问题,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func asyncAdd(lhs: Int, rhs: Int, completion: @escaping (Int) -> Void) {
// 一些代码

// 完成后执行逃逸闭包
completion(lhs + rhs)
}

func asyncAddForGroup(
group: DispatchGroup,
lhs: Int,
rhs: Int,
completion: @escaping (Int) -> Void) {
// 表示开始进入异步方法
group.enter()
asyncAdd(lhs: lhs, rhs: rhs) { result in
// 确保一定会进入leave,通知异步方法执行结束
defer { group.leave() }
completion(result)
}
}

信号量

信号量可以控制资源的并发访问。以下代码会顺序输出0到9:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func run() {
// 创建信号量,1表示最多只能1个线程访问资源
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue.global()
for i in 0..<10 {
queue.async {
// 等待资源释放
semaphore.wait()
print(i)
// 任务结束,可以执行下一个等待任务
semaphore.signal()
}
}
}
run()

Operation

Operation构建于GCD之上,提供了operation依赖、取消正在执行的operation等功能。每个operation有以下几种状态:

  • isReady:实例化完成,准备执行
  • isExecuting:当调用start方法,operation进入该状态
  • isCancelled:当调用cancel方法,operation进入该状态
  • isFinished:如果operation正常结束,进入该状态

两种创建方式

1. BlockOperation
可以使用封装好的BlockOperation来新建operation。BlockOperation类似DispatchGroup,可以添加多个任务,每个任务会被并发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
let sentence = "Say hello to gcfeng"
let wordOperation = BlockOperation()
for word in sentence.split(separator: " ") {
wordOperation.addExecutionBlock {
print(word)
}
}
// 当所有任务结束之后,执行
wordOperation.completionBlock = {
print("All word printed")
}
// 启动
wordOperation.start()
2. 继承Operation
创建Operation子类对象,然后将对象添加到OperationQueue队列中,一旦对象被加入到队列中,队列就开始处理该对象,直到对象的所有操作都执行完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
func run() {
// 创建Operation
let downloadOperation = DownloadOperation()
// 创建队列
let queue = OperationQueue()
queue.addOperation(downloadOperation)
}
}
class DownloadOperation: Operation {
override func main() {
print("download")
}
}
Test().run()

管理依赖

Operation的优势在于封装了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
class DownloadOperation: Operation {
override func main() {
print("download")
}
}
class UploadOperation: Operation {
override func main() {
// 检查依赖是否被取消或者自己被取消了
guard !dependencies.contains(where: { $0.isCancelled }), !isCancelled else {
return
}
print("upload")
}
}
// 实例化两个Operation
let downloadOperation = DownloadOperation()
let uploadOperation = UploadOperation()
// 定义uploadOperation依赖于downloadOperation
// 那么uploadOperation需要等待downloadOperation完成
uploadOperation.addDependency(downloadOperation)
uploadOperation.completionBlock = {
print("upload complete")
}
// 将Operation都添加到OperationQueue中
let queue = OperationQueue()
queue.addOperations([downloadOperation, uploadOperation], waitUntilFinished: false)