当遵循协议或覆盖超类方法时,您可能无法将方法更改为async
,但仍可能希望调用一些async
代码。例如,当我重写一个程序以使用Swift的新结构化并发时,我想通过覆盖XCTestCase
上定义的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并发:幕后中,运行时工程师Rokhini Prabhu指出:
像信号量和条件变量这样的原语在Swift并发中使用是不安全的。这是因为它们隐藏了依赖信息,但在代码执行中引入了依赖。这违反了线程前进的运行时约定。
她还包括了一个此类不安全代码模式的代码片段。
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
async {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
这明显是我想出来的准确模式(我觉得很有趣,因为我想出来的代码与重命名后的经典不正确代码完全一样)。
不幸的是,我没有找到任何其他方法可以从同步函数中等待异步代码完成。此外,我没有找到任何方法可以在同步函数中获取异步函数的返回值。我能在互联网上找到的唯一解决方案似乎和我的一样错误,例如这篇The Swift Dev文章说:
为了在同步方法中调用异步方法,你必须使用新的detach函数,并且你仍然需要使用dispatch API等待异步函数完成。
我认为这是不正确的,或者至少是不安全的。
有没有一种正确、安全的方式可以等待一个async
函数从同步函数中工作,以适应现有的同步类或协议要求,而不具体针对测试或XCTest?或者,我在哪里可以找到关于async
/await
在Swift中与现有同步原语如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
,它就不会卡住。我的一个担忧是,即使函数本身没有标记 @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()
}
如果这对你来说没有卡死,请尝试添加更多的unsafeWaitFor
。我的开发虚拟机有5个核心,而这里有6个unsafeWaitFor
。对我来说,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 数量相同的unsafeWaitFor
”,但如果是这样的话,我想在某个地方看到苹果明确说明。在更复杂的程序中,我真的可以确定我的代码可以访问机器上的所有内核吗?还是可能我的程序的其他部分正在使用其他内核,因此由 unsafeWaitFor
请求的工作将永远不会被调度?
当然,我问题中的示例是关于测试的,因此在那种情况下,很容易说“无论建议是什么都没关系:如果它有效,它就有效,如果不是,测试失败,你会解决它的”,但我的问题不仅仅是关于测试;那只是一个例子。
通过 GCD,我对自己能够使用信号量(在我自己控制的DispatchQueue
上,而不是在主线程上)来同步异步代码而不耗尽总可用线程感到有信心。我希望能够使用 Swift 5.5 中的 async
/await
同步从同步函数中的 async
代码。
如果这样做不可能,我也接受苹果的文档,在其中明确说明我可以在哪些情况下安全使用 unsafeWaitFor
或类似的同步技术。
async/await
的情况下,我们该如何做到这一点?我们无法做到。 没有async/await
,我们从未能够等待,现在仍然不能。 如果我们在setUp
中进行异步工作,setUp
将会结束。 - mattasync
进行所有的测试,效果非常好。我相信将现有的方法切换为async
会破坏ABI和源代码,所以我不知道苹果公司将如何修复setUp
。希望很快就会有一个安全的解决方案。 - deaton.dgasync
/await
。 - deaton.dg