有并发的地方就存在线程安全问题,尤其是对于 Swift 这种还没有内置并发支持的语言来说线程安全问题更为突出。下面我们通过常见的数组操作来分析其中存在的线程问题,以及如何实现一个线程安全数组。
PS :有兴趣的加入Android工程师交流QQ群:752016839 主要针对Android开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。
问题所在
因为无法确定执行顺序,所以并发导致的问题一般都很难模拟和测试。不过我们可以通过下面这段代码来模拟一个并发情形下导致的数据竞争问题。
var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 1000) { index in
let last = array.last ?? 0
array.append(last + 1)
}复制代码
这段代码中我们对数组array进行了 1000 次并发修改操作,虽然有些夸张但是它能很好的揭示一些并发环境下数组写操作存在的一些问题。因为对于值类型来说 Swift 采用的是 Copy On Write 机制,所以在进行 Copy On Write 处理是可能数组已经被另一个写操作给修改了。这就造成了数组中元素和数据的丢失现象,如下:
Unsafe loop count: 988.
Unsafe loop count: 991.
Unsafe loop count: 986.
Unsafe loop count: 995.复制代码
串行队列
这应该是大家都能想到的一种最常见处理方式。 由于串行队列每次都只能运行一个进程,所以即使有多个数组写操作进程我们也能确保资源的互斥访问。这样数组是从设计的并发进程安全的。
let queue = DispatchQueue(label: "SafeArrayQueue")
queue.async() {
// 写操作
}
queue.sync() {
// 读操作
}复制代码
由于写操作并不需要返回操作结果,所有这里可以使用异步的方式进行。而对于读操作来说则必须采用同步的方式实时返回操作结果。但是串行队列有一个最为明显的缺陷:多个读操作之间也是互斥的。很显然这种方式太过粗暴存在明显的性能问题,毕竟读操作的频率直觉上是要高过写操作的。
并发队列
采用并发队列我们就可以很好的解决上面提到的多个读操作的性能问题,不过随之而来的就是写操作的数据竞争。这与我们在学习操作系统是的
读者-作者
问题本质上是一类问题,我们可以通过共享互斥锁来解决写操作的数据竞争问题。对于 iOS 来说它就是 GCD 中的写栏栅
barrier
机制。
let queue = DispatchQueue(label: "SafeArrayQueue", attributes: .concurrent)
queue.async(flags: .barrier) {
// 写操作
}
queue.sync() {
// 读操作
}复制代码
上面代码中我们对异步的写操作设置了
barrier
标示,这意味着在执行异步操作代码的时候队列不能执行其他代码。而对于同步的读操作来说,由于是并发队列同时读取数据并不会存在任何性能问题。