首页 > 解决方案 > Swift:线程安全的单例,为什么我们使用同步进行读取?

问题描述

在制作线程安全的 Singleton 时,建议使用同步进行读取,使用带屏障的异步进行写入操作。

我的问题是为什么我们使用同步读取?如果我们使用异步操作执行读取会发生什么?

以下是推荐的示例:

func getUser(id: String) throws -> User {
  var user: User!
  try concurrentQueue.sync {
    user = try storage.getUser(id)
  }
  return user
}
func setUser(_ user: User, completion: (Result<()>) -> Void) {
  try concurrentQueue.async(flags: .barrier) {
    do {
      try storage.setUser(user)
      completion(.value(())
    } catch {
      completion(.error(error))
    }
  }
}

标签: swiftgrand-central-dispatch

解决方案


使用并发队列的概念与“同时读取” sync;write with barrier with async”是一种非常常见的同步模式,称为“读写器”。这个想法是并发队列仅用于将写入与屏障同步,但读取将与其他读取同时发生。

所以,这里有一个简单的、真实的例子,使用读写器同步访问一些私有状态属性:

enum State {
    case notStarted
    case running
    case complete
}

class ComplexProcessor {
    private var readerWriterQueue = DispatchQueue(label: "...", attributes: .concurrent)

    // private backing stored property
    private var _state: State = .notStarted

    // exposed computed property synchronizes access using reader-writer pattern
    var state: State {
        get { readerWriterQueue.sync { _state } }
        set { readerWriterQueue.async { self._state = newValue } }
    }

    func start() {
        state = .running
        DispatchQueue.global().async {
            // do something complicated here

            self.state = .complete
        }
    }
}

考虑:

let processor = ComplexProcessor()
processor.start()

然后,稍后:

if processor.state == .complete {
    ...
}

计算state属性使用读写器模式提供对底层存储属性的线程安全访问。它同步对某些内存位置的访问,我们相信它会响应。在这种情况下,我们不需要令人困惑@escaping的闭包:sync读取会生成非常简单且易于推理的代码。


话虽如此,在您的示例中,您不仅要与某些属性同步交互,还要与storage. 如果那是保证响应的本地存储,那么读写器模式可能很好。

但是,如果storage方法的运行时间可能超过几毫秒,那么您就不想使用读写器模式。可以抛出错误的事实getUser让我想知道是否storage已经在进行复杂的处理。即使它只是从某个本地存储快速读取,如果它后来被重构为与某个远程存储交互,会受到未知网络延迟/问题的影响怎么办?归根结底,假设值总是会很快返回,getUser让方法对 的实现细节做出假设是有问题的。storage

在这种情况下,您将按照Jeffery Thomas 的建议重构getUser方法以使用@escaping完成处理程序闭包。我们永远不想有一个可能花费超过几毫秒的同步方法,因为我们永远不想阻塞调用线程(尤其是如果它是主线程)。


顺便说一句,如果你坚持读写模式,你可以简化你的getUser, 因为sync返回它的闭包返回的任何值:

func getUser(id: String) throws -> User {
    return try concurrentQueue.sync {
        try storage.getUser(id)
    }
}

而且您不能tryasync(仅在您的do-catch块内)一起使用。所以它只是:

func setUser(_ user: User, completion: (Result<()>) -> Void) {
    concurrentQueue.async(flags: .barrier) {
        do {
            try storage.setUser(user)
            completion(.value(())
        } catch {
            completion(.error(error))
        }
    }
}

推荐阅读