gds*_*gds 47 swift swift-concurrency
我正在 Swift 中桥接同步/异步世界,并逐步采用异步/等待。我正在尝试调用一个从非异步函数返回值的异步函数。我知道显式使用Task是执行此操作的方法,例如,如此处所述。
该示例并不真正适合,因为该任务不返回值。
经过多次搜索,我无法找到任何我认为很常见的问题的描述:异步任务的同步调用(是的,我知道这可能会冻结主线程)。
理论上我想在同步函数中编写的是:
let x = Task {
return await someAsyncFunction()
}.result
Run Code Online (Sandbox Code Playgroud)
但是,当我尝试这样做时,由于尝试访问而出现此编译器错误result:
'async' property access in a function that does not support concurrency
我发现的一种替代方案是这样的:
Task.init {
self.myResult = await someAsyncFunction()
}
Run Code Online (Sandbox Code Playgroud)
其中myResult必须归属为@State成员变量。
然而,这并没有按照我想要的方式工作,因为不能保证在Task.init()完成并进入下一个语句之前完成该任务。那么我怎样才能同步等待该任务完成呢?
Cos*_*syn 30
您不应该同步等待异步任务。
人们可能会想出一种与此类似的解决方案:
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
Task {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
Run Code Online (Sandbox Code Playgroud)
尽管它可以在一些简单的条件下工作,但根据WWDC 2021 Swift Concurrency: Behind the faces,这是不安全的。原因是系统希望您遵守运行时合同。合同要求
线程总是能够前进。
这意味着线程永远不会阻塞。当异步函数到达挂起点(例如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()
}
Run Code Online (Sandbox Code Playgroud)
这里我们在函数中添加了 100 个并发子任务testGroup,不幸的是任务组永远不会完成。在我的 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() {
}
}
Run Code Online (Sandbox Code Playgroud)
这里调用该updateSettings函数会死锁。因为它同步等待updateUser函数,而updateUser函数与同一个 actor 隔离,所以它updateSettings先等待完成。
上面两个例子都使用了DispatchSemaphore. NSCondition出于同样的原因,以类似的方式使用也是不安全的。基本上同步等待意味着阻塞当前线程。除非您只想要临时解决方案并且完全了解风险,否则请避免这种模式。
除了使用信号量之外,您还可以将异步任务包装在如下所示的操作中。您可以在底层异步任务完成后发出操作完成信号,并使用以下命令等待操作完成:waitUntilFinished()
let op = TaskOperation {
try await Task.sleep(nanoseconds: 1_000_000_000)
}
op.waitUntilFinished()
Run Code Online (Sandbox Code Playgroud)
请注意,使用semaphore.wait()或op.waitUntilFinished()阻止当前线程并阻止线程可能会导致现代并发中未定义的运行时行为,因为现代并发假设所有线程始终向前推进。如果您计划仅在不使用现代并发的上下文中使用此方法,则使用 Swift 5.7,您可以提供在异步上下文中不可用的属性标记方法:
@available(*, noasync, message: "this method blocks thread use the async version instead")
func yourBlockingFunc() {
// do work that can block thread
}
Run Code Online (Sandbox Code Playgroud)
通过使用此属性,您只能从非异步上下文调用此方法。但需要注意的是,如果该方法未指定可用性,您可以调用从异步上下文调用此方法的非异步方法noasync。
小智 -9
我也一直好奇这个问题。例如,如何启动一个(或多个)任务并等待它们在主线程中完成?这可能类似于 C++ 的思维,但也必须有一种方法可以在 Swift 中做到这一点。无论好坏,我想出了使用全局变量来检查工作是否完成:
import Foundation
var isDone = false
func printIt() async {
try! await Task.sleep(nanoseconds: 200000000)
print("hello world")
isDone = true
}
Task {
await printIt()
}
while !isDone {
Thread.sleep(forTimeInterval: 0.1)
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
42349 次 |
| 最近记录: |