【发布时间】:2021-08-28 18:17:11
【问题描述】:
当符合协议或重写超类方法时,您可能无法将方法更改为async,但您可能仍想调用一些async 代码。例如,当我正在重写要根据 Swift 的新结构化并发编写的程序时,我想通过覆盖在 @987654328 上定义的 class func setUp() 在我的测试套件的开头调用一些 async 设置代码@。我希望我的设置代码在任何测试运行之前完成,因此使用Task.detached 或async { ... } 是不合适的。
最初,我写了一个这样的解决方案:
final class MyTests: XCTestCase {
override class func setUp() {
super.setUp()
unsafeWaitFor {
try! await doSomeSetup()
}
}
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
这似乎运作良好。然而,在Swift concurrency: Behind the scenes 中,运行时工程师 Rokhini Prabhu 指出
信号量和条件变量等原语在 Swift 并发中使用是不安全的。这是因为它们对 Swift 运行时隐藏了依赖信息,但在代码的执行中引入了依赖...这违反了线程向前推进的运行时契约。
她还包含了这种不安全代码模式的代码 sn-p
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
async {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
这显然是我想出的确切模式(我发现我想出的代码正是规范的不正确代码模重命名非常有趣)。
不幸的是,我无法找到任何其他方法来等待异步代码从同步函数完成。此外,我还没有找到任何方法来获取同步函数中异步函数的返回值。我在互联网上找到的唯一解决方案似乎和我的一样不正确,例如 The Swift Dev article 说
为了在同步方法中调用异步方法,您必须使用新的分离函数,并且您仍然需要使用调度 API 等待异步函数完成。
我认为这是不正确的或至少是不安全的。
什么是等待来自同步函数的async 函数以处理现有同步类或协议要求的正确、安全的方法,而不是特定于测试或 XCTest?或者,我在哪里可以找到说明 Swift 中 async/await 与现有同步原语(如 DispatchSemaphore)之间交互的文档?它们永远不安全,还是我可以在特殊情况下使用它们?
更新:
根据@TallChuck 的回答,注意到setUp() 总是在主线程上运行,我发现我可以通过调用任何@MainActor 函数来故意使程序死锁。这是应尽快更换我的解决方法的绝佳证据。
明确地说,这是一个挂起的测试。
import XCTest
@testable import Test
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
unsafeWaitFor {
try! await doSomeSetup()
}
}
}
func doSomeSetup() async throws {
print("Starting setup...")
await doSomeSubWork()
print("Finished setup!")
}
@MainActor
func doSomeSubWork() {
print("Doing work...")
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
但是,如果 @MainActor 被注释掉,它不会挂起。我担心的一个问题是,如果我调用库代码(Apple 或其他),即使函数本身没有标记为 @MainActor,也无法知道它是否最终会调用 @MainActor 函数。
我的第二个担心是,即使没有@MainActor,我仍然不知道我能保证这是安全的。在我的电脑上,这会挂起。
import XCTest
@testable import Test
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
print("Hello")
}
}
}
}
}
}
}
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
如果这不适合您,请尝试添加更多 unsafeWaitFors。我的开发虚拟机有 5 个内核,这是 6 个unsafeWaitFors。 5对我来说很好。这与 GCD 明显不同。这是 GCD 中的等价物,它不会挂在我的机器上。
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
print("Hello")
callback()
}
callback()
}
callback()
}
callback()
}
callback()
}
callback()
}
}
}
func safeWaitFor(_ f: @escaping (() -> ()) -> ()) {
let sema = DispatchSemaphore(value: 0)
DispatchQueue(label: UUID().uuidString).async {
f({ sema.signal() })
}
sema.wait()
}
这很好,因为 GCD 很乐意生成比 CPU 更多的线程。所以也许建议是“只使用与 CPU 一样多的unsafeWaitFors”,但如果是这样的话,我想看看苹果在哪里明确说明了这一点。在一个更复杂的程序中,我是否可以确定我的代码可以访问机器上的所有内核,或者我的程序的其他部分是否可能正在使用其他内核,因此 unsafeWaitFor 要求的工作永远不会被安排?
当然,我的问题中的示例是关于测试的,因此在这种情况下,很容易说“建议是什么并不重要:如果有效,则有效,如果无效” t,测试失败,你会修复它,”但我的问题不仅仅是关于测试;这只是一个例子。
使用 GCD,我对自己能够在不耗尽可用线程总数的情况下将异步代码与信号量(在我自己控制的 DispatchQueues,而不是主线程上)同步的能力充满信心。我希望能够在 Swift 5.5 中将来自同步函数的 async 代码与 async/await 同步。
如果这样的事情是不可能的,我也会接受来自 Apple 的文档,说明在哪些情况下我可以安全地使用 unsafeWaitFor 或类似的同步技术。
【问题讨论】:
-
我们在之前
async/await会怎么做呢?我们不能。没有async/await,我们永远都等不及了,现在也等不及了。如果我们在setUp期间进行异步工作,setUp将结束。 -
@matt 我们(或至少我)使用上面的
DispatchSemaphore方法,但使用的是回调函数而不是async函数。使用基于DispatchQueue的并发性,这没关系,因为如果队列阻塞,GCD 可以产生更多线程来完成工作,这样阻塞的线程将来可能能够恢复。 Swift 的内置执行器不会产生新线程(至少不是因为这个),所以DispatchSemaphore方法很容易与async函数发生死锁,至少在理论上是这样。我的设置代码很简单,我还没有遇到死锁。 -
在“在 Swift 中遇见 async/await”session 他们指出“XCTest 支持开箱即用的异步”(时间戳 21:20),但它看起来并不包括
setUp(). -
是的。我一直在使用
async进行所有测试,效果很好。我很确定将现有方法切换到async是 ABI 和破坏源,所以我真的不知道 Apple 将如何修复setUp。希望很快会有一个安全的解决方法。 -
为什么不继续做你正在做的事情,保持不变?我不赞成,但是,嘿,如果您对此感到满意,那很好;没有法律要求您的所有代码都从 GCD 等迁移出去。
标签: swift async-await grand-central-dispatch swift5.5