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