Swift:线程安全的单例,为什么我们使用同步进行读取?



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

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

以下是建议的示例:

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

使用并发队列的概念 "与sync并发读取 ;使用屏障写入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的实现细节做出假设是值得怀疑的,假设该值总是会快速返回。

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


顺便说一下,如果你坚持使用读写器模式,你可以简化你的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))
}
}
}

一切都在你想要的。通过将 get user 更改为异步,则需要使用回调来等待值。


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,因此现在在调用 get user 时,需要使用回调。

而不是这样的东西

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()的方法

相关内容

  • 没有找到相关文章

最新更新