iOS 多线程开发之系列文章
多线程开发是日常开发任务中不可缺少的一部分,在 iOS 开发中常用到的多线程开发技术有 GCD、Operation
、Thread
,本文主要讲解多线系列文章中关于 GCD
的相关知识和使用详解。
基础
Grand Central Dispatch 简称 GCD,苹果官方推荐给开发者使用的首选多线程解决方案。多线程开发涉及的细节非常多,下面我会用例子细致的讲解 GCD,请一定要精读,一定要用 Xcode 或 Playground 多次运行代码去对比结果。实践出真知,练习完这篇文章,你一定会觉得精通 Swift 多线程原来很简单。
DispatchWorkItem
调度工作项:其实就是一项任务,可以把你想要执行的代码写成闭包,在DispatchWorkItem初始化时传进去,方便后续管理任务,并且会让代码更整洁。
官网原文:The work you want to perform, encapsulated in a way that lets you attach a completion handle or execution dependencies.
调度工作项初始化,正常情况下,使用第一种方式即可(特殊情况后续会再讲解):
// 1. 只带尾随闭包
let item1 = DispatchWorkItem {
print("item1")
}
// 2. 指定 qos(执行优先级)或 flags(特殊行为标记)
let item2 = DispatchWorkItem(qos: .userInteractive, flags: .barrier) {
print("item2")
}
DispatchQueue
调度队列:一个对象,用来管理任务在 app 的主线程或后台线程串行或并行执行。
官网原文:An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.
DispatchQueue 有三种类型:
- Main Queue
- Global Queue
- Custom Queue
Main Queue(主队列,串行)
Main Queue 与主线程关联的调度队列,是一种串行队列(Serial),与 UI 相关的操作必须放在 Main Queue 中执行,获取方式是:
let mainQueue = DispatchQueue.main
Global Queue(全局队列,并行)
Global Queue 运行在后台线程,是系统内共享的全局队列,是一种并行队列(Concurrent),用于处理并发任务,获取方式是:
let globalQueue = DispatchQueue.global()
Custom Queue(自定义队列,默认串行)
Custom Queue 运行在后台线程,默认是串行队列(Serial),初始化时指定 attributes
参数为 .concurrent
,可以创建成并行队列(Concurrent),创建方式如下:
//串行队列,label名字随便取
let serialQueue = DispatchQueue(label: "test")
//并行队列
let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)
DispatchGroup
调度组:一个小组,你可以把多项任务放到一个组里,方便进行统一管理(直译过来并不好理解)。
官网原文:A group of tasks that you monitor as a single unit.
DispatchGroup 可以很方便的管理多项任务。比如当同一组里的所有事件都完成后,GCD API 可以发送通知,执行相应的操作。常用方法:
-
notify()
调度组里的所有任务执行完毕,会在此收到通知,不会阻塞当前线程。
-
wait()
一直等待,直到调度组里所有任务都执行完毕或等待超时,阻塞当前线程。
实战
使用 DispatchQueue
新建 Playground 项目,定义四个调度任务,提供给下文调用,可大幅降低下文代码量,部分运行结果请自己复制代码多次运行感受,我只讲结果:
import Foundation
//定义四个调度任务,打印当前线程数据
let item1 = DispatchWorkItem {
for i in 0...4{
print("item1 -> (i) thread: (Thread.current)")
}
}
let item2 = DispatchWorkItem {
for i in 0...4{
print("item2 -> (i) thread: (Thread.current)")
}
}
let item3 = DispatchWorkItem {
for i in 0...4{
print("item3 -> (i) thread: (Thread.current)")
}
}
let item4 = DispatchWorkItem {
for i in 0...4{
print("item4 -> (i) thread: (Thread.current)")
}
}
异步执行
//主队列追加异步任务,按顺序打印
let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)
//全局队列追加异步任务,随机打印
let globalQueue = DispatchQueue.global()
globalQueue.async(execute: item1)
globalQueue.async(execute: item2)
globalQueue.async(execute: item3)
globalQueue.async(execute: item4)
//自定义串行队列追加异步任务,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.async(execute: item1)
serialQueue.async(execute: item2)
serialQueue.async(execute: item3)
serialQueue.async(execute: item4)
//自定义并行队列追加异步任务,随机打印
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.async(execute: item1)
concurrentQueue.async(execute: item2)
concurrentQueue.async(execute: item3)
concurrentQueue.async(execute: item4)
注:在串行队列中执行异步任务,结果跟执行同步任务完全一样
同步执行
//主队列追加同步任务,会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)
mainQueue.sync(execute: item2)
mainQueue.sync(execute: item3)
mainQueue.sync(execute: item4)
//全局队列追加同步任务,按顺序打印
let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item1)
globalQueue.sync(execute: item2)
globalQueue.sync(execute: item3)
globalQueue.sync(execute: item4)
//自定义串行队列追加同步任务,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item1)
serialQueue.sync(execute: item2)
serialQueue.sync(execute: item3)
serialQueue.sync(execute: item4)
//自定义并行队列追加同步任务,按顺序打印
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item1)
concurrentQueue.sync(execute: item2)
concurrentQueue.sync(execute: item3)
concurrentQueue.sync(execute: item4)
注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。
同步异步混合执行
//主队列同步异步混合,会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)//同步任务
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)
//全局队列同步异步混合,同步任务按顺序打印,异步任务随机打印
//本例中同步任务执行完,才会执行后续的异步任务
let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item1)//同步任务
globalQueue.async(execute: item2)
globalQueue.async(execute: item3)
globalQueue.async(execute: item4)
//自定义串行队列同步异步混合,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item1)//同步任务
serialQueue.async(execute: item2)
serialQueue.async(execute: item3)
serialQueue.async(execute: item4)
//自定义并行队列同步异步混合,同步任务按顺序打印,异步任务随机打印
//本例中同步任务执行完,才会执行后续的异步任务
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item1)//同步任务
concurrentQueue.async(execute: item2)
concurrentQueue.async(execute: item3)
concurrentQueue.async(execute: item4)
注:
在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务,会引起死锁 |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
死锁分析
主队列死锁
上文提到了主队列不能混入同步任务,否则会引起死锁,为何呢?因为主队列是串行队列,并且仅能运行在主线程上,它无法去创建新的线程,也就意味着所有的代码都必须在只能在一个线程上运行。
正常情况下,主队列上存在源源不断的异步任务(比如用来不断刷新 UI 的任务,用 A 表示),如果混入同步任务(用 B 表示),如果 B 在 A 之后,从时间上看,B 执行完才能执行 A;而从空间上看,A 执行完才能执行 B。两个任务都很有礼貌,相互等待、相互谦让,谁也不好意思先执行,于是就引起了死锁,导致程序卡死崩溃。
官网原文:Attempting to synchronously execute a work item on the main queue results in deadlock.
//会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.sync(execute: item4)//同步任务
有人可能会想,如果 A 在 B 之后呢?是不是就不会引起死锁?看起来不会死锁,可惜 Playground 运行这样的代码,每次都崩溃,应该是程序刚运行,主队列就存在我们看不到的异步任务。
//依然会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)//同步任务
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)
因此只能认为:主队列上不能存在同步任务,否则一定会引起死锁。
其他队列死锁
上文提到主队列死锁,那其他类型的队列会不会引起死锁呢?下面来试一下:
- 自定义串行队列嵌套同步任务,会引起死锁
let serialQueue = DispatchQueue(label: "serial")
//死锁
serialQueue.sync {
print("同步执行 thread: (Thread.current)")
serialQueue.sync {
print("同步执行 thread: (Thread.current)")
}
}
//死锁
serialQueue.async {
print("异步执行 thread: (Thread.current)")
serialQueue.sync {
print("同步执行 thread: (Thread.current)")
}
}
//不会引起死锁
serialQueue.sync {
print("同步执行 thread: (Thread.current)")
serialQueue.async {
print("异步执行 thread: (Thread.current)")
}
}
//不会引起死锁
serialQueue.async {
print("异步执行 thread: (Thread.current)")
serialQueue.async {
print("异步执行 thread: (Thread.current)")
}
}
- 并行队列嵌套同步任务,不会引起死锁
//自定义并行队列(全局并行队列结果一样)
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//不会引起死锁
concurrentQueue.async {
print("异步执行 thread: (Thread.current)")
concurrentQueue.sync {
print("同步执行 thread: (Thread.current)")
}
}
//不会引起死锁
concurrentQueue.sync {
print("同步执行 thread: (Thread.current)")
concurrentQueue.sync {
print("同步执行 thread: (Thread.current)")
}
}
//不会引起死锁
concurrentQueue.sync {
print("同步执行 thread: (Thread.current)")
concurrentQueue.async {
print("异步执行 thread: (Thread.current)")
}
}
//不会引起死锁
concurrentQueue.async {
print("异步执行 thread: (Thread.current)")
concurrentQueue.async {
print("异步执行 thread: (Thread.current)")
}
}
死锁总结
通过上文可以看到,自定义串行队列嵌套同步任务,也是可以引起死锁的,所以死锁不是主队列的专利。但为什么会引起死锁,核心原因是什么?运行下面的代码看看结果:
print("=> 开始执行")
let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)//异步任务
print("=> 执行完毕1")
let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item2)//同步任务
print("=> 执行完毕2")
let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item3)//同步任务
print("=> 执行完毕3")
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item4)//同步任务
print("=> 执行完毕all")
运行结果:
=> 开始执行
=> 执行完毕1
item2 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕2
item3 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕3
item4 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕all
item1 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
看出什么问题没有?四组代码的运行结果完全一样,连线程信息也都一摸一样,都是运行在主线程上(main thread),并且第一组的代码放在了最后执行。也就是说:
- 主队列上的所有任务(只有可能是异步任务)和其他队列的同步任务都运行在主线程上(主线程有且只有一个)。
- 线程不在乎任务是同步还是异步,只有队列才在乎。
- 线程不会死锁,只有队列才会死锁。
主队列添加同步任务会造成死锁的根本原因是:
- 主队列只能运行在主线程(重要的事情再说一遍)。
- 主队列没有本事开启后台线程去干别的事情。
- 主队列一旦混入同步任务,就会跟已经存在的异步任务相互等待,导致死锁。
自定义串行队列添加同步任务不会死锁,因为:
自定义串行队列有能力启动主线程和后台线程(只能启动一个后台线程)。 自定义串行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。
并行队列添加同步任务不会死锁,因为:
并行队列有能力启动主线程和后台线程(可以启动一个或多个后台线程,部分设备上可以启动多达 64 个后台线程)。 并行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。
自定义串行队列一个异步或同步任务(A)嵌套另一个同步任务(B)会引起死锁,因为:
A、B 任务等效为:A1 -> B -> A2,B是同步任务,B 在 A1 之后、A2 之前,B 必须等 A2 执行完才能执行,A2 必须等 B 执行完才能执行,A2 执行完才算 A 执行完了,逻辑上已经陷入死循环,两者相互等待,导致死锁。所以,串行队列不能嵌套同步任务,否则会引起死锁。
DispatchQueue切换
背景介绍
这一章来模拟网络请求:在 APP 中请求网络数据(任务 A: 耗时 10s),获取数据后进行一定的处理(任务 B: 耗时 5s),最后刷新 UI。
假如A和B都是同步任务,放主队列会死锁,而放其他任何队列,界面都会卡死 15s,如果不信,把下面代码里的两种线程休眠方法(二选一,其实不止这两种),放在 APP UIViewController 里试试:
override func viewDidAppear(_ animated: Bool) {
//1. 全局队列执行同步任务
DispatchQueue.global().sync {
sleep(15)//当前线程休眠15秒
}
//2. 主队列执行异步任务
DispatchQueue.main.async {
sleep(15)//当前线程休眠15秒
}
}
不出所料,两种方法,均让界面卡死 15s。回想一下上文说过的:所有的同步任务最终都要安排到主线程运行,主线程运行长耗时任务都会导致界面严重卡顿,所以:
能异步执行的长耗时任务,千万不要同步执行。 长耗时同步任务欠下的债,都由界面来偿还。
假如 A 和 B 都是异步任务,即使这样,你也不能都放在主队列中处理,这样也会导致 APP 界面卡住 15s,因为上面说到了:主线程运行长耗时任务都会导致界面严重卡顿。
所有的长耗时任务,千万不要放在主队列中执行。 主队列长耗时异步任务欠下的债,也都由界面来偿还。
说了那么多,你现在应该能够深切地理解各种队列的运行原理了。
网络请求实例
现在讲讲使用 GCD 多线程处理网络请求的正确做法:A、B 都定义成异步任务,在并行队列中嵌套异步任务,最后切换到主队列去刷新 UI,这样做界面可以保证最流畅。
//创建并行队列,尽量用自定义队列,免得自己的代码质量不过关,影响全局队列
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
//异步执行
queue.async {
print("开始请求数据 (Date()) thread: (Thread.current)")
sleep(10)//模拟网络请求
print("数据请求完成 (Date()) thread: (Thread.current)")
//异步执行
queue.async {
print("开始处理数据 (Date()) thread: (Thread.current)")
sleep(5)//模拟数据处理
print("数据处理完成 (Date()) thread: (Thread.current)")
//切换到主队列,刷新UI
DispatchQueue.main.async {
print("UI刷新成功 (Date()) thread: (Thread.current)")
}
}
}
运行结果:
开始请求数据 2020-08-06 06:40:57 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
数据处理完成 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
UI刷新成功 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff917c0e7e0>{number = 1, name = main}
可以看到队列和线程均进行了预期的切换,GCD 队列切换像俄罗斯套娃一样,一层一层的嵌套就行,等嵌套出问题了,滑到上面在 死锁分析 里寻找原因进行修改即可。
使用 DispatchGroup
如果希望多项任务执行完毕后,再去执行另一项任务,可以使用DispatchGroup。这些任务可以放在同一队列中,也可以放在不同队列中。
DispatchGroup常用的方法:
-
group.wait()
阻塞当前线程,一直到 group 所有任务执行完毕。
-
group.notify()
所有任务执行完毕后,异步发送通知,不阻塞当前线程。
使用 group.notify() 改写一下上一章网络请求的例子:
let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
//异步执行
queue.async(group: group) {
print("开始请求数据 (Date()) thread: (Thread.current)")
sleep(10)//模拟网络请求
print("数据请求完成 (Date()) thread: (Thread.current)")
//异步执行
queue.async(group: group) {
print("开始处理数据 (Date()) thread: (Thread.current)")
sleep(5)//模拟数据处理
print("数据处理完成 (Date()) thread: (Thread.current)")
}
}
print("开始监听")
//在当前队列监听
group.notify(queue: queue) {
//切换到主队列,刷新UI
DispatchQueue.main.async {
print("UI刷新成功 (Date()) thread: (Thread.current)")
}
}
print("监听完毕")
运行结果:
开始监听
监听完毕
开始请求数据 2020-08-06 06:45:22 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
UI刷新成功 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312c0e7e0>{number = 1, name = main}
如你所愿,运行结果跟上文一致。
精简代码,直接在主队列监听通知、刷新 UI:
let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
//异步执行
queue.async(group: group) {
print("开始请求数据 (Date()) thread: (Thread.current)")
sleep(10)//模拟网络请求
print("数据请求完成 (Date()) thread: (Thread.current)")
//异步执行
queue.async(group: group) {
print("开始处理数据 (Date()) thread: (Thread.current)")
sleep(5)//模拟数据处理
print("数据处理完成 (Date()) thread: (Thread.current)")
}
}
print("开始监听")
//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
print("UI刷新成功 (Date()) thread: (Thread.current)")
}
print("监听完毕")
运行结果:
开始监听
监听完毕
开始请求数据 2020-08-06 06:49:31 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
UI刷新成功 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608c0e7e0>{number = 1, name = main}
如你所愿,运行结果依然一致。
使用 group.wait() 改写:
let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
//异步执行
queue.async(group: group) {
print("开始请求数据 (Date()) thread: (Thread.current)")
sleep(10)//模拟网络请求
print("数据请求完成 (Date()) thread: (Thread.current)")
//异步执行
queue.async(group: group) {
print("开始处理数据 (Date()) thread: (Thread.current)")
sleep(5)//模拟数据处理
print("数据处理完成 (Date()) thread: (Thread.current)")
}
}
print("开始监听")
//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
print("UI刷新成功 (Date()) thread: (Thread.current)")
}
group.wait()//阻塞当前线程
print("监听完毕")
运行结果:
开始监听
开始请求数据 2020-08-06 06:53:00 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
监听完毕
UI刷新成功 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1ad40e7e0>{number = 1, name = main}
可以看到 group.wait() 的确阻塞了当前线程。
进阶篇
DispatchGroup 挂起、恢复
在第7章的例子里,嵌套了三层,还不算多,但是已经可以隐约感受到嵌套地狱了。这一节用队列挂起、恢复重写,解决嵌套问题。以后遇到更多层级的嵌套,可以用同样的方法解决。
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
let queue2 = DispatchQueue(label: "com.apple.response", attributes: .concurrent)
queue2.suspend()//队列挂起
//异步执行
queue1.async(group: group) {
print("开始请求数据 (Date()) thread: (Thread.current)")
sleep(10)//模拟网络请求
print("数据请求完成 (Date()) thread: (Thread.current)")
queue2.resume()//网络数据请求完成,恢复队列,进行数据处理
}
//异步执行
queue2.async(group: group) {
print("开始处理数据 (Date()) thread: (Thread.current)")
sleep(5)//模拟数据处理
print("数据处理完成 (Date()) thread: (Thread.current)")
}
print("开始监听")
//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
print("UI刷新成功 (Date()) thread: (Thread.current)")
}
print("监听完毕")
线程安全
如果有一个变量有可能被多个线程同时读写,结果便不可预期,必须进行特殊处理,来保证线程安全。
通过 barrier 标识设置屏障
自定义队列支持 DispatchWorkItem
设置 flags
为 .barrier
,可以支持 barrier
之前的任务全部执行完毕后,再执行 .barrier
任务,最后再执行 .barrier
之后的任务,这样处理可以保证线程安全。
注:全局队列,flags 设置 .barrier 无效
import Foundation
let item1 = DispatchWorkItem {
for i in 0...4{
print("item1 -> (i) thread: (Thread.current)")
}
}
let item2 = DispatchWorkItem {
for i in 0...4{
print("item2 -> (i) thread: (Thread.current)")
}
}
//给item3任务加barrier标识
let item3 = DispatchWorkItem(flags: .barrier) {
for i in 0...4{
print("item3 barrier -> (i) thread: (Thread.current)")
}
}
let item4 = DispatchWorkItem {
for i in 0...4{
print("item4 -> (i) thread: (Thread.current)")
}
}
let item5 = DispatchWorkItem {
for i in 0...4{
print("item5 -> (i) thread: (Thread.current)")
}
}
let queue = DispatchQueue(label: "test", attributes: .concurrent)
queue.async(execute: item1)
queue.async(execute: item2)
queue.async(execute: item3)
queue.async(execute: item4)
queue.async(execute: item5)
运行结果:
item1 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item2 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item4 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item4 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item5 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item5 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
使用 DispatchSemaphore 给线程上锁
DispatchSemaphore 被很多人翻译成信号量,说实话我这辈子第一次听说信号量,信号还有量?什么量?多少量?
吐槽完毕,为了方便理解,在这里我把它临时翻译成红绿灯吧。
DispatchSemaphore 初始化时只有一个参数 value(通行数量),表示还可以通行几辆车(还可以执行几个异步任务)。
DispatchSemaphore 有两个方法:
-
wait()
执行一次,通行数量减 1,通行数量为 0 时就表示红灯,全都得等着
-
signal()
执行一次,通行数量加1
举一个 99 乘法表的例子,感受下 DispatchSemaphore:
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//执行9个异步任务
for i in 1...9 {
queue.async {
semaphore.wait()//通行数量减1,此处变为0,红灯,全都得等着
var str = ""
for j in 1...9{
//格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
let value = i * j
let tempStr = value <= 9 ? " (value) " : "(value) "
str += tempStr
}
print(str)
semaphore.signal()//通行数量加1,后面可继续通行
}
}
运行结果:
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
99乘法表显示理想
注释掉 semaphore.wait() 和 semaphore.signal(),多运行几次试试看:
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//执行9个异步任务
for i in 1...9 {
queue.async {
//semaphore.wait()//通行数量减1,此处变为0,红灯,全都得等着
var str = ""
for j in 1...9{
//格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
let value = i * j
let tempStr = value <= 9 ? " (value) " : "(value) "
str += tempStr
}
print(str)
//semaphore.signal()//通行数量加1,后面可继续通行
}
}
运行结果:
5 10 15 20 25 30 35 40 45
4 8 12 16 20 24 28 32 36
3 6 9 12 15 18 21 24 27
1 2 3 4 5 6 7 8 9
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
2 4 6 8 10 12 14 16 18
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
99乘法表已经失控
为了更深刻的理解,试试把上面的例 1 中 DispatchSemaphore 初始化时 value 设为 2 或 3,多次运行下程序看看结果,你能感受到通行数量对失控程度的影响。
使用串行队列 + 计算属性,修改变量
import Foundation
let queue = DispatchQueue(label: "test")
var a:Int = 10
var b:Int{
get{
queue.sync {
print("同步读取 thread = (Thread.current)")
return a
}
}
set{
queue.sync {
print("同步写入 thread = (Thread.current)")
a = newValue
}
}
}
b = 30//赋值
print("a = (a) b = (b) thread = (Thread.current)")
运行结果:
同步写入 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
同步读取 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
a = 30 b = 30 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
尝试修改set为异步写入,思索下结果。
DispatchQoS
DispatchQoS 调度优先级:直译过来就是应用在任务上的服务质量或执行优先级,可以理解为任务的身份、等级。可以用来修饰 DispatchWorkItem
、DispatchQueue
。
就像航空公司有身份的客户,在VIP休息室等飞机、坐头等舱、高质量空姐贴心服务等等,最好的服务优先都给你;如果你没有身份、只有身份证,平平安安的到达目的地就可以知足了;如果你连身份证也没有,那就去坐公交车吧。
官网原文:The quality of service, or the execution priority, to apply to tasks.
DispatchQoS 有以下几种类型:
-
userInteractive
与用户交互相关的任务,要最重视,优先处理,保证界面最流畅
-
userInitiated
用户主动发起的任务,要比较重视
-
default
默认任务,正常处理即可
-
utility
用户没有主动关注的任务
-
background
不太重要的维护、清理等任务,有空能处理完就行
-
unspecified
别说身份了,连身份证都没有,能处理就处理,不能处理也无所谓的
DispatchQoS 其实只是一个简单的优先级标识,为何会放在进阶篇里说呢?
因为对于绝大部分开发者来说,没必要设置这个标识,设置了也只是徒增代码复杂度,花里胡哨的技巧用了一大堆,代码量不小,最后到处都是 bug,有意义吗?
还是尽量让代码简单点、少出问题最好,很多书里都讲:代码越少,bug 越少。当有一天你想增强用户体验、提高代码运行效率、优化设备能耗,说明你的应用质量、代码档次都已经很不错了,明显属于进阶水准,这时你应该去试试这个标识了。所以,鄙人认为,DispatchQoS 属于进阶内容。
在 DispatchWorkItem 上添加 DispatchQoS 标识:
import Foundation
let item1 = DispatchWorkItem(qos: .userInteractive) {
for i in 0...9999{
print("--item1 -> (i) thread: (Thread.current)")
}
}
let item2 = DispatchWorkItem(qos: .unspecified) {
for i in 0...9999{
print("item2 -> (i) thread: (Thread.current)")
}
}
let queue = DispatchQueue(label: "test1", attributes: .concurrent)
queue.async(execute: item1)
queue.async(execute: item2)
运行结果显示 item1 执行完了,item2 才开始打印 3824。
for 循环次数需要调大一些,否则效果不明显。
在 DispatchQueue 上添加 DispatchQoS 标识:
import Foundation
let item1 = DispatchWorkItem {
for i in 0...9999{
print("--item1 -> (i) thread: (Thread.current)")
}
}
let item2 = DispatchWorkItem {
for i in 0...9999{
print("item2 -> (i) thread: (Thread.current)")
}
}
let queue1 = DispatchQueue(label: "test1",qos: .userInteractive, attributes: .concurrent)
let queue2 = DispatchQueue(label: "test2", qos: .unspecified, attributes: .concurrent)
queue1.async(execute: item1)
queue2.async(execute: item2)
我这边运行结果显示 item1 执行完了,item2 才开始打印 3798。
for 循环次数不用太大,效果也可以很明显,您可以自己探索一下。
结束语
要精通 Swift 多线程,还是要多在实践中使用,在使用过程中反复思索、反复优化,这项技术很快就会成为你的拿手好戏。 多线程虽好,但请不要滥用,不要为了炫技去用多线程,毕竟当前的 CPU 性能已经非常高,每秒钟可执行万亿次级别的操作,而屏幕每秒钟仅仅刷新几十、上百次,眨眼的功夫大量的代码就执行完了。在必要的地方再去用多线程吧,代码整洁、问题少、应用稳定可靠才更重要。
注意事项
-
开发多线程时,养成一个习惯,时刻打印
Thread.current
,看看代码是不是运行在自己预期的线程上。 -
如果遇到单例之间相互调用,务必注意 DispathQueue 嵌套问题。调用另一个单例时,必要时用
DispatchQueue.main.async{ }
包裹一下,将其切换回主线程,然后在该单例内部的方法中,再进行线程切换。否则线程多层嵌套后,代码将会失控,试图挽救也会无从下手。