提问人:Parth 提问时间:11/10/2023 更新时间:11/11/2023 访问量:123
如何使用 actor 允许并行读取,但阻止对 Swift 中的资源进行并发读取和写入?
How to use actors to allow parallel reads but block concurrent reads and writes to a resource in Swift?
问:
因此,我使用并发调度队列和屏障标志来支持并行读取,但在写入时阻止读取/写入:
struct SafeDict<Element> {
private var dict = [String: Element]()
private var queue = DispatchQueue(label: "WriteQueue", attributes: .concurrent)
func get(_ key: String) -> Element? {
queue.sync { return dict[key] }
}
mutating func set(_ key: String, value: Element) {
queue.sync(flags: .barrier) { dict[key] = value }
}
}
var safeDict = SafeDict<Int>()
for i in 1...4 {
DispatchQueue.global().async {
switch i {
case 1:
safeDict.get("one")
case 2:
safeDict.set("one", value: 1) // Waits for get (good)
case 3:
safeDict.get("one") // Runs after set in parallel
case 4:
safeDict.get("two") // Runs after set in parallel
default:
print("done")
}
}
}
但是,由于 actor 函数是异步的,因此并行读取将相互等待。如何才能干净利落地避免这种情况?
我能想到的一种选择是不使用actor,而是使用2个异步方法(get set)。如果不是 nil,get 将等待设定的任务。但这似乎太乏味了。
actor SafeDictActor<Element> {
private var dict = [String: Element]()
func get(_ key: String) -> Element? { dict[key] }
func set(_ key: String, value: Element) { dict[key] = value }
}
let safeDictActor = SafeDictActor<Int>()
for i in 1...4 {
Task {
switch i {
case 1:
await safeDictActor.get("one")
case 2:
await safeDictActor.set("one", value: 1) // Waits for get (good)
case 3:
await safeDictActor.get("one") // waits for set (good)
case 4:
await safeDictActor.get("two") // waits for previous get (bad)
default:
print("done")
}
}
}
答:
正如 Paulw11 在最近一个问题的评论中提到的,如何用快速并发解决读/写器问题? (您在问题中注意到),actor 使用一种基本的同步机制,该机制本质上与读写器模式不兼容。Actor基于一种简单(但优雅)的机制,该机制通过避免对共享状态的任何并行访问来防止竞争,但读取器-写入器是基于允许并行读取的模式。读写器并行读取的想法与 actor 的基本机制完全不兼容。
话虽如此,我还要补充几点意见:
如果你真的想要 reader-writer(我不确定你为什么要这样做;参见下面的第 3 点),你可以坚持使用 GCD 来处理该对象。是的,我们通常避免将 GCD 与 Swift 并发代码库混合在一起,但这是可以做到的(特别是如果划分为单一类型,而不是在单个类型中混合不同的技术堆栈)。除非绝对必要,否则我一般不会建议这样做,但这是可以做到的。
但是,如果将其与 Swift 并发代码库集成,您可以考虑将此类型与 .Swift 并发性采用代码线程安全的编译时验证(尤其是将“严格并发”构建设置设置为“完成”;实际上是我们将在 Swift 6 中享受的那种检查的预览)。有效地让编译器知道您正在保证此对象的线程安全。它可以让您的对象在 Swift 并发中很好地运行。
@unchecked Sendable
@unchecked Sendable
有关
Sendable
的更多信息,WWDC 2022 的视频 Eliminate data races using Swift Concurrency 是该主题的入门读物。我注意到您的读写器实现正在使用值类型: 但是值类型的整个想法是提供一种简单的线程安全机制,通过该机制,我们为每个线程提供自己的对象副本,从而消除潜在的竞争。但是,读取器-写入器的想法是允许从多个线程对同一对象进行线程安全共享访问。现在,您可以有一个读取器-写入器值类型,但有点奇怪地说,您既希望线程安全访问以跨线程共享对象,又希望为每个线程提供自己的副本。在某些情况下,您可能会这样做,但许多读写用例需要共享的可变状态,这需要引用语义。(顺便说一句,您考虑的 actor 实现也使用引用语义。
我理解读者-作者模式的直观吸引力。但根据我的经验,基于 GCD 的实现通常无法实现其承诺。在我所有的基准测试中,(a) 它的性能仅比 GCD 串行队列高一点;(b) 它通常比简单的锁慢得多;(c)它引入的问题往往多于解决的问题。例如,如果按照您的设想,您有 1000 次读取,则存在更深层次的线程爆炸问题,而读取器-写入器只会使该问题复杂化。根据我的经验,GCD 的开销可能会开始超过并行执行的潜在好处。我曾徒劳地试图构建现实的场景,在这些场景中,读者-作者比简单的不公平锁更快。我建议对你的特定用例进行基准测试,并避免假设读写器会更快。
总而言之,Actor对于高级线程的安全性/完整性非常有用。在性能至关重要的情况下(例如,Knuth 设想的 3% 场景,性能确实至关重要,例如计算密集型并行化算法),我个人跳过读写器并跳到性能更高的内容(例如,不公平的锁)。但对于大多数实际用例来说,actor 绰绰有余。
你说:
但是,由于 actor 函数是异步的,因此并行读取将相互等待。如何才能干净利落地避免这种情况?
我能想到的一种选择是不使用actor,而是使用2个异步方法(get set)。如果不是 nil,get 将等待设定的任务。但这似乎太乏味了。
actor SafeDictActor<Element> { private var dict = [String: Element]() func get(_ key: String) -> Element? { dict[key] } func set(_ key: String, value: Element) { dict[key] = value } }
我要撇开你建议的“不使用actor”,然后向我们展示了一个actor实现。我猜你的意思是“使用演员”。我还打算抛开 not are 的建议,因为它们不是。 (是的,当你调用它们时需要它们,但它们是 actor 的同步函数。
get
set
async
await
但是,演员确实优雅地为我们同步了这一点。不需要 GCD 队列或锁。只需简单地让它成为,你就完成了。但你是对的,两个调用不能并行运行。执行组件将一次执行一个函数。这就是它确保螺纹安全的方式。
actor
get
对于绝大多数情况,这种简单的执行组件实现就足够了。但是,对于关键的 3% 的情况,性能确实是最重要的问题,只有这样,我们才会考虑其他模式。
评论
NSCache
@unchecked Sendable
struct SafeDict<Element>: @unchecked Sendable
Sendable
@unchecked Sendable
@unchecked
@unchecked Sendable