使用 Swift 中的新并发将同步函数转换为异步函数

huy*_*ync 3 async-await swift

我想将同步函数转换为异步函数,但我不知道正确的方法是什么。

假设我有一个需要很长时间才能获取数据的同步函数:

func syncLongTimeFunction() throws -> Data { Data() }
Run Code Online (Sandbox Code Playgroud)

然后我在下面的函数中调用它,它仍然是一个同步函数。

func syncGetData() throws -> Data {
    return try syncLongTimeFunction()
}
Run Code Online (Sandbox Code Playgroud)

但现在我想将其转换为异步函数。下面正确的做法是什么:

  • 第一种方式:

    func asyncGetData() async throws -> Data {
        return try syncLongTimeFunction()
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 第二种方式:

    func asyncGetData2() async throws -> Data {
        return try await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global().async {
                do {
                    let data = try self.syncLongTimeFunction()
                    continuation.resume(returning: data)
                } catch {
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

我认为第一种方法就足够了,就像罗布在下面的回答一样。但是,当我在主线程上调用如下所示的异步函数时,该函数syncLongTimeFunction()也会在主线程上进行处理。所以它会阻塞主线程。

 async {       
     let data = try? await asyncGetData()
 }
Run Code Online (Sandbox Code Playgroud)

Rob*_*Rob 9

简而言之,这取决于慢速/同步函数的性质:

\n
    \n
  1. 它是否是一些相对短暂的同步任务,而您正试图避免 UI 中出现短暂的故障?

    \n

    在这种情况下,常见的建议通常是 \xe2\x80\x9cdetached\xe2\x80\x9d 任务:

    \n
    func asyncGetData() async throws -> Data {\n    try await Task.detached {\n        try self.someSyncFunction()\n    }.value\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    但这是非结构化并发,因此您需要自己承担支持任务取消的负担。所以像下面这样的东西可能更合适:

    \n
    func asyncGetData() async throws -> Data {\n    let task = Task.detached {\n        try self.longTimeFunction()\n    }\n    return try await withTaskCancellationHandler {\n        try await task.value\n    } onCancel: {\n        task.cancel()\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    (不用说,这假设longTimeFunctionEven 支持取消。下面会详细介绍。它还假设longTimeFunction对相关参与者来说不是孤立的;您可能想确保它是非孤立的。)

    \n

    或者,从 Swift 5.7 开始(感谢SE-0338),您可以享受结构化并发并使用非隔离async函数将其从当前 actor 中获取:

    \n
    nonisolated func asyncGetData() async throws -> Data {\n    try self.longTimeFunction()\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    因为这是结构化并发,所以我们免费获得取消传播。而且因为它是非隔离async函数,所以它可以从当前参与者中获取它。

    \n

    为了完整起见,还有其他选择。但这些是从当前演员手中夺取任务的一些常见方法。

    \n
  2. \n
  3. 这个缓慢的函数是否执行一些计算密集型计算?

    \n

    为了确保 actor 始终能够取得 \xe2\x80\x9cforward 进度\xe2\x80\x9d,您需要定期Task.yield. 在这种情况下,您还需要确保它也使用checkCancellation或定期检查取消情况isCancelled

    \n

    例如:

    \n
    func longTimeFunction() async throws -> Data {\n    \xe2\x80\xa6\n\n    for i in \xe2\x80\xa6 {\n        try Task.checkCancellation()\n        await Task.yield()\n        \xe2\x80\xa6\n    }\n\n    return \xe2\x80\xa6\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    现在,您检查取消和收益的频率由您决定。我通常会在计算速度和我想要检查的频率之间取得一些平衡(例如,if i.isMultiple(of: 100) {\xe2\x80\xa6}),但是,希望您能明白这一点。

    \n
  4. \n
  5. 或者它确实是一个缓慢的同步函数,您无法轻松重构以支持 Swift 并发?

    \n

    然后,我们必须意识到,Swift 并发是基于契约的,以确保任务始终能够取得进展。

    \n

    具体来说,SE-0296 - Async/await警告我们,我们要么必须提供某种方式在 Swift 并发中交错(例如,使用Task.yield前面提到的方法),要么我们\xe2\x80\x9c 通常在单独的上下文中运行它\ xe2\x80\x9d:

    \n
    \n

    由于潜在的挂起点只能出现在异步函数中显式标记的点处,因此长计算仍然会阻塞线程。当调用只执行大量工作的同步函数时,或者遇到直接在异步函数中编写的特别密集的计算循环时,可能会发生这种情况。在任何一种情况下,线程都无法在这些计算运行时交错代码,这通常是正确性的正确选择,但也可能成为可扩展性问题。需要进行密集计算的异步程序通常应该在单独的上下文中运行。

    \n
    \n

    该 Swift 演化提案没有明确定义 \xe2\x80\x9crun 它在单独的上下文 \xe2\x80\x9d 中。但在 WWDC 2022 视频Visualize and optimization Swift concurrency中,他们建议 GCD:

    \n
    \n

    如果您有需要执行这些操作的代码,请将该代码移动到并发线程池 \xe2\x80\x93 之外,例如,通过在调度队列 \xe2\x80\x93 上运行它,并使用以下命令将其桥接到并发世界延续。只要有可能,请使用异步 API 进行阻塞操作,以保持系统平稳运行。

    \n
    \n

    简而言之,在这种情况下,您将采用选项 2,将 GCD 代码封装在withCheckedThrowingContinuation. 显然,我们必须担心 GCD 的所有传统问题(例如,减轻线程爆炸、手动确保线程安全、避免死锁等)仍然适用。请注意,当您开始使用 GCD API 占用 CPU 核心时,这超出了 Swift 并发协作线程池的范围,因此您仍然可能会遇到 CPU 过度使用,而该池旨在缓解这一问题。

    \n
  6. \n
\n

因此,底线完全取决于同步和/或长时间运行例程的性质。不幸的是,如果没有更多关于 的信息,就不可能更具体syncLongTimeFunction

\n