在迁移到结构化并发时,有时临时对异步函数进行阻塞调用可能很方便--例如:
let x: Int = try blocking {
try await Task.sleep(nanoseconds: 1)
return 5
}
print(x)我现在已经这样做了,但我并不喜欢它:
@available(*, deprecated, message: "For use only as a last resort or to allow incremental migration to structured concurrency")
public func blocking<A>(_ ƒ: @escaping () async -> A) -> A {
let semaphore = DispatchSemaphore(value: 0)
let got = Got<A>()
Task(priority: .high) {
got.a = await ƒ()
semaphore.signal()
}
semaphore.wait()
return got.a!
}还有一个投掷变体:
@available(*, deprecated, message: "For use only as a last resort or to allow incremental migration to structured concurrency")
public func blocking<A>(_ ƒ: @escaping () async throws -> A) throws -> A {
let semaphore = DispatchSemaphore(value: 0)
let got = Got<Result<A, Error>>()
Task(priority: .high) {
do { got.a = try await .success(ƒ()) }
catch { got.a = .failure(error) }
semaphore.signal()
}
semaphore.wait()
return try got.a!.get()
}其中,Got只是一个简单的类,用于保存对结果的引用:
private class Got<A> {
var a: A?
}在回顾了标准库的并发模块之后,我现在认为没有一种惯用的表达方式:“我可以在等待这个特定的异步调用时阻止我的同步函数”。
发布于 2022-01-14 09:03:45
我知道您认为这只是迁移过程中的一个临时步骤,但我完全不鼓励使用这种模式。在Swift并发中使用信号量是不安全的。
在WWDC 2021视频快速并发:幕后中,苹果讨论了“保存运行时合同”和确保“向前进展”的必要性,并正式禁止使用信号量:
另一方面,像信号量和条件变量这样的原语在Swift并发中使用是不安全的。这是因为它们向Swift运行时隐藏依赖信息,但在代码中引入了执行中的依赖项。
这是一个很有启发性的视频,我可能会建议完整地观看它。
关于是否存在用现有API实现这一目标的“技巧”,我担心答案是,最好是简单地重构代码,这样就不需要这种模式了。如果您有一些倾向于使用上述代码的代码,我可能建议将其作为一个单独的问题发布,并且我们可以帮助您解决这个问题,而无需使用信号量。
发布于 2022-01-14 12:32:47
建议的方法同时挂在iOS和macOS上(只是不是在并行执行的单元测试中),这本身就很吸引人。见内联注释:
@main
struct BlockerApp: App {
@State var count = 0
var body: some Scene {
WindowGroup {
VStack {
Text("\(count)")
Button("Increment") { // tapping on the button hangs the app
count += blocking { 1 } // the closure is never called
}
}
}
}
}
func blocking<A>(_ ƒ: @escaping () async -> A) -> A {
let semafore = DispatchSemaphore(value: 0)
let got = Got<A>()
Task.detached(priority: .high) {
got.a = await ƒ() // gets as far as this line but ƒ is never called
semafore.signal()
}
semafore.wait()
return got.a!
}
private class Got<A> {
var a: A?
}但是,如果我们要求在另一个上下文中执行闭包,例如通过使用自定义的全局参与者对参数进行注释,则此解决方案在macOS和iOS上都有效:
@globalActor public actor BlockingActor {
public static let shared = BlockingActor()
}
@available(*, deprecated, message: "For use only to allow incremental migration to structured concurrency")
public func blocking<A>(_ ƒ: @BlockingActor @escaping () async -> A) -> A {
let semaphore = DispatchSemaphore(value: 0)
let got = Got<A>()
Task(priority: .high) {
got.a = await ƒ()
semaphore.signal()
}
semaphore.wait()
return got.a!
}唉,嵌套调用仍然会导致挂起:
count += blocking {
blocking { // moving to the same executor, so we hang as before
1
}
}当然,这只是一个特殊的例子,说明了必须禁止使用任何此类API的根本原因。但是,请注意,我们已经有相当多的现有API类似地被禁止,但确实存在是有充分理由的。
一个特别相关的例子是sync方法的DispatchQueue,如果您使用它来调度任务以等待自身,它将以完全相同的方式导致挂起。尽管如此,多年来sync方法还是得到了很好的应用。
https://codereview.stackexchange.com/questions/272943
复制相似问题