在遵守协议或覆盖超类方法时,您可能无法将方法更改为 async
,但您可能仍想调用一些 async
代码。例如,当我正在根据 Swift 的新结构化并发重写一个程序时,我想通过覆盖 async
在测试套件的开头调用一些设置代码在 class func setUp()
上定义。我希望我的设置代码在任何测试运行之前完成,因此使用 XCTestCase
或 Task.detached
是不合适的。
最初,我写了一个这样的解决方案:
async { ... }
这似乎工作得很好。但是,在 Swift concurrency: Behind the scenes 中,运行时工程师 Rokhini Prabhu 指出
<块引用>像信号量和条件变量这样的原语在 Swift 并发中使用是不安全的。这是因为它们对 Swift 运行时隐藏了依赖信息,但在您的代码中引入了执行时的依赖......这违反了线程前进的运行时契约。
她还包含了这样一个不安全代码模式的代码片段
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()
}
这显然是我想出的确切模式(我觉得很有趣,我想出的代码正是规范的错误代码模重命名)。
不幸的是,我找不到任何其他方法来等待异步代码从同步函数中完成。此外,我还没有找到任何方法来获取同步函数中异步函数的返回值。我在互联网上找到的唯一解决方案似乎和我的一样不正确,例如这个 The Swift Dev article 说
<块引用>为了在同步方法中调用异步方法,你必须使用新的分离函数,你仍然需要等待异步函数使用调度 API 完成。
我认为这是不正确的或至少是不安全的。
等待来自同步函数的 func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
async {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
函数处理现有同步类或协议要求的正确、安全的方法是什么,不特定于测试或 XCTest?或者,我在哪里可以找到详细说明 Swift 中的 async
/async
与现有同步原语(如 await
)之间的交互的文档?它们永远不会安全,或者我可以在特殊情况下使用它们吗?
根据@TallChuck 的回答,注意到 DispatchSemaphore
总是在主线程上运行,我发现我可以通过调用任何 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()
}
被注释掉,它不会挂起。我的一个担心是,如果我调用库代码(Apple 的或其他的),即使函数本身没有标记为 {{1},也无法知道它最终是否会调用 @MainActor
函数}.
我的第二个恐惧是,即使没有 @MainActor
,我仍然不知道我保证这是安全的。在我的电脑上,这挂起。
@MainActor
如果这对您没有影响,请尝试添加更多 @MainActor
。我的开发 VM 有 5 个内核,这是 6 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()
}
秒。 5 对我来说很好用。这明显不同于 GCD。这是 GCD 中的等效项,它不会挂在我的机器上。
unsafeWaitFor
这很好,因为 GCD 很乐意生成比 CPU 多的线程。所以也许建议是“只使用与 CPU 一样多的 unsafeWaitFor
”,但如果是这种情况,我希望看到 Apple 在某处明确说明了这一点。在一个更复杂的程序中,我真的可以确定我的代码可以访问机器上的所有内核,或者我的程序的其他部分是否可能正在使用其他内核,因此 {{1 }} 永远不会被安排?
当然,我的问题中的示例是关于测试的,因此在这种情况下,很容易说“建议是什么并不重要:如果有效,则有效,如果无效” t,测试失败,你会修复它,”但我的问题不仅仅是关于测试;那只是一个例子。
使用 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()
}
上,而不是主线程上)的能力充满信心,而不会耗尽可用线程总数。我希望能够将来自同步函数的 unsafeWaitFor
代码与 Swift 5.5 中的 unsafeWaitFor
/DispatchQueue
同步。
如果这样的事情是不可能的,我也会接受 Apple 提供的文档,详细说明我可以安全地使用 async
或类似同步技术的具体情况。
答案 0 :(得分:0)
您可能会争辩说异步代码不属于 setUp()
,但在我看来,这样做会将 同步性 与 顺序... icity? setUp()
的重点是在其他任何东西开始运行之前运行,但这并不意味着它必须同步编写,只是其他一切都需要将其视为依赖项。 >
幸运的是,Swift 5.5 引入了一种处理代码块之间依赖关系的新方法。它被称为 await
关键字(您可能听说过)。关于 async
/await
(在我看来)最令人困惑的事情是它造成的鸡与蛋的双面问题,在我能够使用的任何材料中都没有得到很好的解决找到。一方面,您只能从已经是异步的代码中运行异步代码(即使用 await
),另一方面,异步代码似乎被定义为任何使用 await
(即运行其他异步代码)。
在最底层,最终必须有一个 async
函数实际执行一些异步操作。从概念上讲,它可能看起来像这样(请注意,虽然以 Swift 代码的形式编写,但这是严格的伪代码):
func read(from socket: NonBlockingSocket) async -> Data {
while !socket.readable {
yieldToScheduler()
}
return socket.read()
}
换句话说,与先有鸡还是先有蛋的定义相反,这个异步函数不是通过使用 await
语句来定义的。它会循环直到数据可用,但它允许自己在等待时被抢占。
在最高级别,我们需要能够启动异步代码而无需等待它终止。每个系统都以单个线程开始,并且必须通过某种引导过程来产生任何必要的工作线程。在大多数应用程序中,无论是在台式机、智能手机、Web 服务器上还是在您拥有的任何设备上,主线程然后进入某种“无限”循环,在那里它可能会处理用户事件或侦听传入的网络连接,然后以适当的方式与工人互动。然而,在某些情况下,程序是要运行到完成的,这意味着主线程需要监督每个工作程序的成功完成。对于传统线程,例如 POSIX pthread
库,主线程为某个线程调用 pthread_join()
,该线程在该线程终止之前不会返回。使用 Swift 并发性,您..... 不能做这样的事情(据我所知)。
structured concurrency 提议允许顶级代码调用 async
函数,或者通过直接使用 await
关键字,或者通过使用 @main
标记一个类,以及定义一个 static func main() async
成员函数。在这两种情况下,这似乎都意味着运行时创建了一个“主”线程,将您的顶级代码作为工作线程启动,然后调用某种 join()
函数以等待它完成。>
如您的代码片段所示,Swift 确实提供了一些允许同步代码创建 Task
的标准库函数。任务是 Swift 并发模型的构建块。您引用的 WWDC 演示文稿解释说,运行时旨在创建与 CPU 内核数量一样多的工作线程。然而,后来他们展示了下图,并解释了在主线程需要运行的任何时候都需要进行上下文切换。
据我了解,线程到 CPU 内核的映射仅适用于“协作线程池”,这意味着如果您的 CPU 有 4 个内核,则实际上总共有 5 个线程。主线程应该保持大部分阻塞状态,因此唯一的上下文切换将是主线程唤醒的极少数情况。
重要的是要了解,在这种基于任务的模型下,控制“继续”切换(与上下文切换不同)的是运行时,而不是操作系统。另一方面,信号量在操作系统级别运行,对运行时不可见。如果您尝试使用信号量在两个任务之间进行通信,则可能会导致操作系统阻塞您的线程之一。由于运行时无法跟踪这一点,因此它不会启动一个新线程来代替它,因此您充其量只会未得到充分利用,最坏的情况是会陷入僵局。
好的,最后,在Meet async/await in Swift中,说明XCTest
库可以“开箱即用”运行异步代码。但是,不清楚这是否适用于 setUp()
,还是仅适用于单个测试用例函数。如果事实证明它确实支持异步 setUp()
函数,那么您的问题突然变得完全无趣了。另一方面,如果它不支持它,那么你就被困在不能直接等待你的 async
函数的位置,但它也不够好,仅仅旋转启动一个非结构化的 Task
(即一个你触发并忘记的任务)。
您的解决方案(我认为这是一种解决方法——正确的解决方案是让 XCTest
支持 async
setUp()
),只阻塞主线程,因此应该是使用安全。