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)
}
}
并且您不能将 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))
}
}
}
在制作线程安全的单例时,建议使用同步进行读操作,使用带有屏障的异步进行写操作。
我的问题是为什么我们使用同步读取?如果我们执行异步读取操作会发生什么?
这是推荐的示例:
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)
}
}
并且您不能将 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))
}
}
}