iOS GCD 实现线程安全的多读单写功能
本文测试 demo 都是在 playground 里用 Swift5 完成的. 使用 GCD实现线程安全修改数据源, 示例中的读写都是对一个字典而言, 实际开发中可以是文件的读写(FileManager 是线程安全的), 可以是数组, 根据自己情况而定.
先来了解一下 GCD 中 队列 , 任务 , 线程, 同步, 异步 之间的关系 和 特点 :
- GCD 默认有两个队列 : 主队列 和 全局队列
- 主队列是特殊的串行队列, 主队列的任务一定在主线程执行.
- 全局队列就是普通的并发队列.
- 队列中的任务遵守先进先出规则, 即 FIFO.
- 队列只调试任务.
- 线程来执行任务.
- 同步执行不具有开启线程的能力
- 异步执行具有开启线程的能力, 但是不一定会开启新线程
- 并发队列允许开启新线程 .
- 串行队列不允许开启新线程的能力.
- 栅栏函数堵塞的是队列.
注意 : 主队列同步执行会造成死锁.
应用场景
- 写
开启多个任务去修改数据, 保证资源不被抢占. 比如买火车票, 多个窗口同时出票, 在服务器只能是一个一个来, 不能出现两个人同时买到同一个座位号的情况, 所以此时我们就需要保证数据安全, 即同一时间只能有一个任务去修改数据.
- 写
- 读
读操作可以允许多个任务同时加入队列, 但是要保证一个一个执行, 此处使用并发同步, 这么做是为了保证按照外部调用顺序去返回结果, 保证当前读操作完成后, 后面的操作才能进行. 其实是个假多读.
- 读
初始化代码
// 并发队列
var queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
// 数据
var dictionary: [String: Any] = [:]
/// 数据初始化
func testInit() {
dictionary = [
"name": "Cooci",
"age": 18,
"girl": "xiaoxiannv"
]
}
读写的关键代码
/// 读的过程
func getSafeValueFor(_ key: String) -> Any? {
var result: Any? = nil
// 并发同步读取数据, 实际是假多读
queue.sync {
result = dictionary[key]
}
return result
}
/// 写的过程
func setSafe(_ value: Any, for key: String) {
// 在子线程完成写任务
// 等待前面任务执行完成后开始写
// 写的完成任务后, 才能继续执行后边添加进此队列的任务
queue.async(flags: .barrier) {
dictionary[key] = value
}
}
首先来看看修改数据 -- 写操作
下面是写操作测试代码和执行结果 :
/// 写的过程
func setSafe(_ value: Any, for key: String) {
queue.async(flags: .barrier) {
dictionary[key] = value
let name = dictionary[key] as? String ?? ""
print("save name = \(name) --> \(Thread.current)")
}
}
/// 测试写的过程
func testWrite() {
setSafe("AAAAA", for: "name")
setSafe("BBBBB", for: "name")
setSafe("CCCCC", for: "name")
print("所有写操作后的任务")
sleep(1)
let name4 = dictionary["name"] ?? "失败"
print("for 后边的代码任务 name4 = \(name4)")
}
我们可以看到 A, B, C 三个操作按照入队的顺序依次执行, 修改数据, name4 取到的是最后一次修改的数据, 这正是我们想要的. 使用并发是为了不堵塞当前线程(当前主线程), 当前线程写操作后面的的代码可以继续执行.
你可能会说, 按照 A, B, C 三个任务添加的顺序输出也不是没可能, 那咱们现在给 setSafe 函数添加一个休眠时长的参数, 让 A 操作休眠 3s, B 休眠 2s, C 休眠 0s, 看看执行顺序是怎样的.
func setSafe(_ value: Any, for key: String, sleepNum: UInt32) {
queue.async(flags: .barrier) {
sleep(sleepNum)
dictionary[key] = value
let name = dictionary[key] as? String ?? ""
print("save name = \(name) --> \(Thread.current)")
}
}
/// 测试写的过程
func testWrite() {
setSafe("AAAAA", for: "name", sleepNum: 3)
setSafe("BBBBB", for: "name", sleepNum: 1)
setSafe("CCCCC", for: "name", sleepNum: 0)
print("所有写操作后的任务")
sleep(5)
let name4 = dictionary["name"] ?? "失败"
print("for 后边的代码任务 name4 = \(name4)")
}
多次执行后的结果都是相同的, 如下图所示 :
由此可见, 添加到队列中的写操作任务(即修改数据源), 只能依次按照添加顺序进行修改, 不会出现资源抢夺现象, 保证了多线程修改数据的安全性.
注意: 此处为什么只有一个线程呢 ?
因为每个任务执行完成后, 队列中已经没有其他任务, GCD 为了节约资源开销, 所以并不会开启新的线程. 也没必要去开启.
再来看看数据的读取 -- 写
并发同步读取数据, 保证外部调用顺序. 此时会堵塞当前线程, 当前线程需要等待读取任务执行完成, 才能继续执行后边代码任务
/// 读的过程
func getSafeValueFor(_ key: String) -> Any? {
var result: Any? = nil
// 在调用此函数的线程同步执行所有添加到 queue 队列的读任务,
// 如果前边有写的任务, 由于 barrier 堵塞队列, 只能等待写任务完成
queue.sync {
result = dictionary[key]
}
return result
}
/// 测试读的过程
func testRead() {
for i in 0...11 {
let order = i % 3
switch order {
case 0:
let name = getSafeValueFor("name") as? String ?? ""
print("\(order) - name = \(name)")
case 1:
let age = getSafeValueFor("age") as? Int ?? 0
print("\(order) - age = \(age)")
case 2:
let girl = getSafeValueFor("girl") as? String ?? "---"
print("\(order) - girl = \(girl)")
default:
break
}
}
print("循环后边的任务")
}
并发异步回调方式读取数据, 当你对外部调用顺序没有要求时, 那你可以这么调用.
/// 读的过程
func getSafeValueFor(_ key: String, completion: @escaping (Any?)->Void) {
queue.async {
let result = dictionary[key]
completion(result)
}
}
func testRead() {
for i in 0...10 {
let order = i % 3
switch order {
case 0:
getSafeValueFor("name") { result in
let name = result as? String ?? "--"
print("\(order) - name = \(name) \(Thread.current)")
}
case 1:
getSafeValueFor("age") { result in
let age = result as? Int ?? 0
print("\(order) - age = \(age) \(Thread.current)")
}
case 2:
getSafeValueFor("girl") { result in
let girl = result as? String ?? "--"
print("\(order) - girl = \(girl) \(Thread.current)")
}
default:
break
}
if i == 5 {
setSafe(100, for: "age")
}
}
print("循环后边的任务")
}
作者:AndyGF
链接:https://www.jianshu.com/p/281b37174dd0