我想将同步函数转换为异步函数,但我不知道正确的方法是什么。
假设我有一个需要很长时间才能获取数据的同步函数:
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)
简而言之,这取决于慢速/同步函数的性质:
\n它是否是一些相对短暂的同步任务,而您正试图避免 UI 中出现短暂的故障?
\n在这种情况下,常见的建议通常是 \xe2\x80\x9cdetached\xe2\x80\x9d 任务:
\nfunc asyncGetData() async throws -> Data {\n try await Task.detached {\n try self.someSyncFunction()\n }.value\n}\n
Run Code Online (Sandbox Code Playgroud)\n但这是非结构化并发,因此您需要自己承担支持任务取消的负担。所以像下面这样的东西可能更合适:
\nfunc 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(不用说,这假设longTimeFunction
Even 支持取消。下面会详细介绍。它还假设longTimeFunction
对相关参与者来说不是孤立的;您可能想确保它是非孤立的。)
或者,从 Swift 5.7 开始(感谢SE-0338),您可以享受结构化并发并使用非隔离async
函数将其从当前 actor 中获取:
nonisolated func asyncGetData() async throws -> Data {\n try self.longTimeFunction()\n}\n
Run Code Online (Sandbox Code Playgroud)\n因为这是结构化并发,所以我们免费获得取消传播。而且因为它是非隔离async
函数,所以它可以从当前参与者中获取它。
为了完整起见,还有其他选择。但这些是从当前演员手中夺取任务的一些常见方法。
\n这个缓慢的函数是否执行一些计算密集型计算?
\n为了确保 actor 始终能够取得 \xe2\x80\x9cforward 进度\xe2\x80\x9d,您需要定期Task.yield
. 在这种情况下,您还需要确保它也使用checkCancellation
或定期检查取消情况isCancelled
。
例如:
\nfunc 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}
),但是,希望您能明白这一点。
或者它确实是一个缓慢的同步函数,您无法轻松重构以支持 Swift 并发?
\n然后,我们必须意识到,Swift 并发是基于契约的,以确保任务始终能够取得进展。
\n具体来说,SE-0296 - Async/await警告我们,我们要么必须提供某种方式在 Swift 并发中交错(例如,使用Task.yield
前面提到的方法),要么我们\xe2\x80\x9c 通常在单独的上下文中运行它\ xe2\x80\x9d:
\n\n由于潜在的挂起点只能出现在异步函数中显式标记的点处,因此长计算仍然会阻塞线程。当调用只执行大量工作的同步函数时,或者遇到直接在异步函数中编写的特别密集的计算循环时,可能会发生这种情况。在任何一种情况下,线程都无法在这些计算运行时交错代码,这通常是正确性的正确选择,但也可能成为可扩展性问题。需要进行密集计算的异步程序通常应该在单独的上下文中运行。
\n
该 Swift 演化提案没有明确定义 \xe2\x80\x9crun 它在单独的上下文 \xe2\x80\x9d 中。但在 WWDC 2022 视频Visualize and optimization Swift concurrency中,他们建议 GCD:
\n\n\n如果您有需要执行这些操作的代码,请将该代码移动到并发线程池 \xe2\x80\x93 之外,例如,通过在调度队列 \xe2\x80\x93 上运行它,并使用以下命令将其桥接到并发世界延续。只要有可能,请使用异步 API 进行阻塞操作,以保持系统平稳运行。
\n
简而言之,在这种情况下,您将采用选项 2,将 GCD 代码封装在withCheckedThrowingContinuation
. 显然,我们必须担心 GCD 的所有传统问题(例如,减轻线程爆炸、手动确保线程安全、避免死锁等)仍然适用。请注意,当您开始使用 GCD API 占用 CPU 核心时,这超出了 Swift 并发协作线程池的范围,因此您仍然可能会遇到 CPU 过度使用,而该池旨在缓解这一问题。
因此,底线完全取决于同步和/或长时间运行例程的性质。不幸的是,如果没有更多关于 的信息,就不可能更具体syncLongTimeFunction
。
归档时间: |
|
查看次数: |
2427 次 |
最近记录: |