在创建线程安全的单例时,建议使用同步进行读取,并使用带有屏障的异步进行写入操作。
我的问题是为什么我们使用同步进行读取?如果我们执行异步读取操作会发生什么?
以下是建议的示例:
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)
}
}
而且您不能将try
与async
结合使用(仅在您的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()
的方法