您不应该同步等待异步任务。
可能有人会提出类似于以下的解决方案:
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
Task {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
尽管在某些简单条件下可以使用,但根据WWDC 2021 Swift Concurrency: Behind the scenes,这是不安全的。原因在于系统期望您遵守运行时合同。该合同要求:
线程始终能够向前推进。
这意味着线程永远不会被阻塞。当异步函数到达挂起点(例如await表达式)时,函数可以被挂起,但线程不会被阻塞,而是可以进行其他工作。基于此合同,新的协作线程池只能生成与CPU核心数量相同的线程,避免过多的线程上下文切换。这也是演员不会导致死锁的关键原因。
以上的信号量模式违反了这个合同。semaphore.wait()
函数会阻塞线程。这可能会引起问题。例如:
func testGroup() {
Task {
await withTaskGroup(of: Void.self) { group in
for _ in 0 ..< 100 {
group.addTask {
syncFunc()
}
}
}
NSLog("Complete")
}
}
func syncFunc() {
let semaphore = DispatchSemaphore(value: 0)
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000)
semaphore.signal()
}
semaphore.wait()
}
在testGroup
函数中,我们添加了100个并发的子任务,但不幸的是,任务组永远无法完成。在我的 Mac 上,系统会生成 4 个协同线程,只需添加 4 个子任务就足以无限期地阻塞所有 4 个线程。这是因为,在所有 4 个线程都被wait
函数阻塞之后,没有更多的线程可用于执行内部任务来发出信号量。
另一个不安全使用的例子是 actor 死锁:
func testActor() {
Task {
let d = Database()
await d.updateSettings()
NSLog("Complete")
}
}
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
Task {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
actor Database {
func updateSettings() {
updateDatabase {
await self.updateUser()
}
}
func updateUser() {
}
}
在这里调用updateSettings
函数将会死锁。因为它会同步等待updateUser
函数完成,而updateUser
函数被隔离到同一个actor中,所以它也在等待updateSettings
函数先完成。
以上两个示例使用了DispatchSemaphore
。基于类似的原因,在NSCondition
中以类似的方式进行等待是不安全的。基本上,同步等待意味着阻塞当前线程。除非您只想要临时解决方案并完全理解风险,否则避免使用此模式。
Task.init
闭包内呢?话虽如此,我认为上述建议是很好的。 - jnpdx