多线程并发有三个比较突出的问题,
- 资源竞争
- 死锁
- 优先级倒置
1. 资源竞争
在不同线程的同时想要更新一个变量,而读写是分开的,那么就可能会出现资源竞争的情况。
CPU是基于时钟信号来工作的, 每个周期只能执行一个单一的操作
iPhone XS 处理器 2.49 GHz, 意味着一秒钟可以执行 2,490,000,000 个周期
例子
线程1和2同时想要更新count
count += 1
这行代码仔细拆分开来会是这样子
- 加载
count
的值到内存中 - 在内存中将
count
值+1
- 将新的
count
值写入磁盘中
图片引自 Raywenderlich.com 的 Concurrency by tutorial
如上图所示,
- 在第一个时钟周期,线程1读取到了
count
的值 - 在第二个时钟周期,线程1在内存中更新了
count
的值,线程2读取到了count
的值 - 在第三个时钟周期,线程1把更新后的值2写入了磁盘,而线程2此时在内存中更新值1为2
- 在第四个时钟周期,线程2把更新后的值2写入了磁盘
此时,count
的值是2而不是3,
多核处理器可以同时处理多个线程,但都是有一个时钟频率驱动工作
事实上,例子中线程1只要在两个时钟周期之前更新就不会有问题了,而现在的iPhone每秒可以执行几十亿次(当然并不是所有写入操作都能这么快完成的)。
所以在实际的项目中,特别是频繁多线程写入读取的时候,会有可能发生这种资源竞争问题。在测试的时候怎么跑都是对的,而放到生产环境中就又出问题了。
解决方案
一般资源竞争问题可以通过
- 串行线程访问
- Thread Barrier
- 线程锁
a. 串行线程访问
对于简单读写操作来说,串行Dispatch可以说是最简单高效的方案了
private let threadSafeCountQueue = DispatchQueue(label: "...") // 默认串行线程
private var _count = 0
public var count: Int {
get {
return threadSafeCountQueue.sync {
_count
}
} set {
threadSafeCountQueue.sync {
_count = newValue
}
}
}
lazy
初始化同样不是线程安全的,如果会有多线程访问并初始化的情况,也可以给这个lazy
变量加一个串行线程来使其线程安全
b. Thread Barrier
图片引自 Raywenderlich.com 的 Concurrency by tutorial
实现
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) { [unowned self] in
self._count = newValue }
}
}
Barrier里的代码会在之前任务执行完之后才会继续,
一旦Barrier触发了,它所在的线程就会像变成串行队列一样,后续的任务要在Barrier的内容执行完之后,才会继续照旧并发运行。
c. 线程锁
基本上所有任务都可以通过上次两种方案解决,直接使用线程锁容易产生问题。
详细可以参考我的另一片文章[Swift] iOS线程锁
2. 死锁
比如你正在开一辆车,双向个一条车道,你临时要调个头。
也就是要等对向车道的车过了之后有空才能转个弯,所以你这条车道后面的车都被你堵住了。
而你没有办法转过去,因为此时对面车道的车也要往你的车道上转弯,但它必须等你走了,让你后面的车走了,它才能转进去。
这时的情况就是死锁。
也就是两个线程或者两个任务结束/开始的条件相互依赖而导致无法进行到下一步
典型的就是在主线程中
DispatchQueue.main.sync(){}
Swfit本身的机制保障了使得我们很少会遇到这种问题,除非我们自己手动调用Semaphore
或者其它线程锁。所以在使用对应的机制的时候经过严谨的测试或者至少深思熟虑一些即可避免这个问题
3. 优先级反转
优先级反转发生在优先级(QoS)较低的队列比优先级较高的队列有更高的系统优先级时。
在本系列的第二篇文章中有提到,将高优先级的任务提交到较低优先级的队列,系统会提升队列的优先级来匹配任务的优先级。
这会导致比如仅有一个.userInteractive
级任务的.utility
级队列甚被系统排到了.userInitiated
前面执行。
解决方案: 仅将相同优先级的任务加入一个队列,其它任务加入到其它队列。
实际上,发生优先级反转更常见的情况是,较高优先级的队列与较低优先级的队列共享资源。当较低的队列锁定该对象时,较高的队列现在必须等待。在释放锁之前,高优先级队列实际上一直处于阻塞状态,而低优先级任务在运行时不执行任何操作。
系列文章链接