swift 多线程GCD实战经验
GCD (Grand Central Dispath
)
回到主线程执行UI刷新
DispatchQueue.global().async {
print("async do something\(Thread.current)")
DispatchQueue.main.async {
print("come back to main thread\(Thread.current)")
}
}
延时执行
时间是可能会有延迟, 而且不能取消.
DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + 2.0) {
print("time out")
}
如果有需要取消延时执行的业务,可以使用DispatchWorkItem
,这个task能执行cancel.
let task = DispatchWorkItem { print("block which can cancel") }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5, execute: task)
task.cancel()
全局队列 的4个优先级qos
User Interactive
和用户交互相关,比如动画等等优先级最高。比如用户连续拖拽的计算User Initiated
需要立刻的结果,比如push一个ViewController之前的数据计算Utility
可以执行很长时间,再通知用户结果。比如下载一个文件,给用户下载进度Background
用户不可见,比如在后台存储大量数据
在GCD中,指定qos有以下两种方式
- 创建一个指定qos的queue
let globalQueue = DispatchQueue.global(qos: .default)
- 在提交block的时候,指定qos
queue.async(group: nil, qos: .background, flags: .inheritQoS) {
}
其中关于qos的关系,可以通过flags参数设置:
- public static let barrier: DispatchWorkItemFlags
- public static let detached: DispatchWorkItemFlags
- public static let assignCurrentContext: DispatchWorkItemFlags
- public static let noQoS: DispatchWorkItemFlags
- public static let inheritQoS: DispatchWorkItemFlags
- public static let enforceQoS: DispatchWorkItemFlags
线程组DispatchGroup
执行多个任务的业务逻辑
1. 简单的多线程任务逻辑:
async(group:)
:封装任务notify()
:通知, 所有任务结束后收到通知, 放在notify里面的block执行, 不阻塞当前线程wait()
:等待直到所有任务执行结束,中途不能取消,阻塞当前线程, 还能设置timeout.
//获取系统存在的全局队列
let queue = DispatchQueue.global(qos: .default)
//定义一个group
let group = DispatchGroup()
//并发任务,顺序执行
queue.async(group: group) {
sleep(3)
print("block1")
}
queue.async(group: group) {
print("block2")
}
queue.async(group: group) {
print("block3")
}
//1,所有任务执行结束汇总,不阻塞当前线程
group.notify(queue: .global(), execute: {
print("group done")
})
//2,永久等待,直到所有任务执行结束,中途不能取消,阻塞当前线程
group.wait()
print("任务全部执行完成")
//打印
//block2
//block3
//block1
//group done
//任务全部执行完成
2. GCD里的Barrier
和NSOperationQueue的dependency比较接近,C任务开始之前需要A任务完成,或者A和B任务完成。
let queue = DispatchQueue(label: "foo", attributes: .concurrent)
queue.async {
print("A")
}
queue.async {
print("B")
}
queue.async(flags: .barrier) {
print("C")
}
3. 用group.enter()和group.leave()来解决带网络请求的线程间异步执行, 也可以使用下面介绍的信号量方式.
Dispatch队列会在闭包内的代码执行完的时候自动通知DispatchGroup. 但是如果你在Dispatch闭包里调用了一个异步请求,Dispatch内的代码很有可能在异步请求完成之前就跑完了,然后就告诉Group"这里的任务已经完成了", 这样会有业务问题.
//这对命令其实就相当于是在计数,当你enter()的时候,运行中的任务计数+1; 当你leave()的时候,运行中的任务-1
let group = DispatchGroup()
group.enter()
self.sendHTTPRequest1(params:[String: Any]) {
print("request complete")
group.leave()
}
group.enter()
self.sendHTTPRequest1(params:[String: Any]) {
print("request complete")
group.leave()
}
group.notify(queue: DispatchQueue.main) {
print("all requests come back")
}
信号量 Semaphore 很多人不会读这个英语单词: [ˈseməfɔː(r)]
DispatchSemaphore(value: )
:初始化信号量计数值0, 他的计数器如果>0, wait函数不会卡住当前线程.semaphore.wait()
:信号量计数器如果>0,则往下执行, 否则一直等待中..., 相当于加锁semaphore.signal()
:信号量加1
多个网络请求的多线程业务特用, 例如等待多个网络请求完成后再做某事, 特好用
//网络请求:
func network(success:()->(), failure:()->()){
}
//开启全局线程
DispatchQueue.global().async {
//创建信号量
let semaphore = DispatchSemaphore.init(value: 0)//初始值为0, 需要>0的时候wait函数才能收到通知
//网络请求1
network(success: {
semaphore.signal()//网络请求结束, semaphore信号+1
}) {
semaphore.signal()//网络请求结束, semaphore信号+1
}
semaphore.wait() //等待到semaphore的计数>0才收到通知往下执行, 执行后semaphore计数自动减去1
//网络请求2
network(success: {
semaphore.signal()//网络请求结束, semaphore信号+1
}) {
semaphore.signal()//网络请求结束, semaphore信号+1
}
semaphore.wait() //等待到semaphore的计数>0才收到通知往下执行执行后semaphore计数自动减去1
print("两个网络请求都结束了, 此时semaphore计数器为0")
}
信号量对比DispatchGroup
- DispatchGroup用于跟踪任务完成状态,然后在批量任务完成/未完成的时候来处理业务逻辑
- DispatchSemorephore用于控制访问数量更加优雅
import Foundation
print("Hello, World!")
//控制只下载3个资源
let semaphore = DispatchSemaphore(value: 3)
for i in 1...10 {
DispatchQueue.global().async{
semaphore.wait() // Semaphore计数-1
print("正在下载第\(i)张图")
// 模拟网络等待
Thread.sleep(forTimeInterval: 2)
print("第\(i)张图已下载")
//semaphore.signal() // Semaphore计数+1 就能多下载一个
}
}
RunLoop.main.run()
打印输出
Hello, World!
正在下载第2张图
正在下载第1张图
正在下载第3张图
第1张图已下载
第2张图已下载
第3张图已下载
多线程并发主要问题:
资源竞争
死锁
优先级倒置
资源竞争
线程1和2同时想要更新一个count: int
这个属性
count += 1
这行代码仔细拆分开来会是这样子
- 加载
count
的值到CPU中 - 在CPU中将
count
值+1
- 将新的 count 值写入内存中
解决方案
- 串行线程访问
- Thread Barrier
- 线程锁
1. 串行线程访问
对于简单读写操作来说,串行Dispatch算是简单高效的方案.
private let queue = DispatchQueue(label: "...") // 默认串行线程
private var _count = 0
public var count: Int {
get {
return queue.sync {
_count
}
}
set {
queue.sync {
_count = newValue
}
}
}
2. Thread Barrier, Barrier后续的任务要在Barrier的内容执行完之后,才会运行。
private let safeQueue = DispatchQueue(label: "...", attributes: .concurrent)
private var _count = 0
public var count: Int {
get {
return safeQueue.sync {
return _count
}
}
set {
safeQueue.async(flags: .barrier) {
[unowned self] in
self._count = newValue }
}
}
上面两种方案可以解决绝大部分业务问题,后面详解线程锁方案.
死锁的产生
sync函数往当前串行队列中添加任务就会造成死锁
//以下代码输出结果是什么?为什么?
override func viewDidLoad() {
super.viewDidLoad()
let queue = DispatchQueue.main
queue.sync {
print("我被锁死了😝")
}
}
- viewDidLoad主线程的一个先行任务, queue.sync是后后添加的任务,需要等上一个任务viewDidLoad执行完才能执行, viewDidLoad执行到queue.sync的时候执行不下去, 构成死锁.
- 将sync函数换成async函数, 不会等待任务执行完,会直接向下执行. async函数具备开启新现成的能力,但是在主线程执行任务,所以不会开启新的线程.
override func viewDidLoad() {
super.viewDidLoad()
print(1111)
let queue = DispatchQueue.main
queue.async {
print(222)
}
print(3333)
}
//打印结果
1111
3333
2222
本文由 创作,采用 知识共享署名4.0 国际许可协议进行许可。本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为: 2021/06/08 06:55