Swift: 线程安全的单例,为什么读要用sync?

Swift: Thread safe Singleton, why do we use sync for read?

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

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

这是推荐的示例:

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))
    }
  }
}

一切尽在您的心意。通过将获取用户更改为异步,然后您需要使用回调来等待值。


func getUser(id: String, completion: @escaping (Result<User>) -> Void) -> Void {
    concurrentQueue.async {
        do {
            let user = try storage.getUser(id)
            completion(.value(user))
        } catch {
            completion(.error(error))
        }
    }
}

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

这改变了获取用户的API,所以现在调用获取用户时,需要使用回调。

而不是像这样的东西

do {
    let user = try manager.getUser(id: "test")
    updateUI(user: user)
} catch {
    handleError(error)
}

你将需要这样的东西

manager.getUser(id: "test") { [weak self] result in
    switch result {
    case .value(let user):  self?.updateUI(user: user)
    case .error(let error): self?.handleError(error)
    }
}

假设你有一个类似视图控制器的东西,它有一个名为 manager 的 属性 和方法 updateUI()handleError()

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

因此,这是一个使用 reader-writer 同步访问某些私有状态的简单、真实的示例 属性:

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 computed 属性 使用 reader-writer 模式提供对底层存储 属性 的线程安全访问。它同步访问一些内存位置,我们相信它会响应。在这种情况下,我们不需要令人困惑的 @escaping 闭包:sync 读取结果是非常简单的代码,易于推理。


话虽如此,在您的示例中,您不只是同步与某些 属性 的交互,而是同步与 storage 的交互。如果这是保证响应的本地存储,那么 reader-writer 模式可能没问题。

但是如果 storage 方法可以花费超过几毫秒到 运行,你就不会想要使用 reader-writer 模式。 getUser 可以抛出错误的事实让我想知道 storage 是否已经在进行复杂的处理。即使它只是从一些本地商店快速读取,如果它后来被重构为与一些远程商店交互,受制于未知网络 latency/issues 怎么办?最重要的是,让 getUser 方法对 storage 的实现细节做出假设是有问题的,假设该值将始终快速返回。

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


顺便说一下,如果你继续使用 reader-writer 模式,你可以简化你的 getUser,因为 sync returns 无论它的闭包值是什么 returns:

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))
        }
    }
}