美文网首页IOS面试专题
iOS GCD 实现线程安全的多读单写功能

iOS GCD 实现线程安全的多读单写功能

作者: AndyGF | 来源:发表于2020-07-16 19:12 被阅读0次

本文测试 demo 都是在 playground 里用 Swift5 完成的. 使用 GCD实现线程安全修改数据源, 示例中的读写都是对一个字典而言, 实际开发中可以是文件的读写(FileManager 是线程安全的), 可以是数组, 根据自己情况而定.

先来了解一下 GCD 中 队列 , 任务 , 线程, 同步, 异步 之间的关系 和 特点 :
  • GCD 默认有两个队列 : 主队列 和 全局队列
  • 主队列是特殊的串行队列, 主队列的任务一定在主线程执行.
  • 全局队列就是普通的并发队列.
  • 队列中的任务遵守先进先出规则, 即 FIFO.
  • 队列只调试任务.
  • 线程来执行任务.
  • 同步执行不具有开启线程的能力
  • 异步执行具有开启线程的能力, 但是不一定会开启新线程
  • 并发队列允许开启新线程 .
  • 串行队列不允许开启新线程的能力.
  • 栅栏函数堵塞的是队列.

注意 : 主队列同步执行会造成死锁.

应用场景


    1. 开启多个任务去修改数据, 保证资源不被抢占. 比如买火车票, 多个窗口同时出票, 在服务器只能是一个一个来, 不能出现两个人同时买到同一个座位号的情况, 所以此时我们就需要保证数据安全, 即同一时间只能有一个任务去修改数据.

    1. 读操作可以允许多个任务同时加入队列, 但是要保证一个一个执行, 此处使用并发同步, 这么做是为了保证按照外部调用顺序去返回结果, 保证当前读操作完成后, 后面的操作才能进行. 其实是个假多读.

初始化代码

// 并发队列
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)")
}
多个写操作任务执行结果.png
  • 我们可以看到 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)")
}

多次执行后的结果都是相同的, 如下图所示 :


三个写操作休眠时间依次递减执行结果.png

由此可见, 添加到队列中的写操作任务(即修改数据源), 只能依次按照添加顺序进行修改, 不会出现资源抢夺现象, 保证了多线程修改数据的安全性.

注意: 此处为什么只有一个线程呢 ?
因为每个任务执行完成后, 队列中已经没有其他任务, 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("循环后边的任务")
}
并发同步执行结果.png

并发异步回调方式读取数据, 当你对外部调用顺序没有要求时, 那你可以这么调用.

/// 读的过程
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("循环后边的任务")
}
并发异步读取结果.png

相关文章

网友评论

    本文标题:iOS GCD 实现线程安全的多读单写功能

    本文链接:https://www.haomeiwen.com/subject/manphktx.html