我想将同步函数转换为异步函数,但我不知道正确的方法是什么。
假设我有一个需要很长时间才能获取数据的同步函数:
func syncLongTimeFunction() throws -> Data { Data() }
然后我在下面的函数中调用它,它仍然是一个同步函数。
func syncGetData() throws -> Data {
return try syncLongTimeFunction()
}
但现在我想将其转换为异步函数。下面正确的做法是什么:
-
第一种方式:
func asyncGetData() async throws -> Data {
return try syncLongTimeFunction()
}
-
第二种方式:
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)
}
}
}
}
我认为第一种方法就足够了,就像罗布在下面的回答一样。但是当我在主线程上调用如下所示的异步函数时,syncLongTimeFunction()
也在主线程上处理。所以它会阻塞主线程。
async {
let data = try? await asyncGetData()
}
简而言之,这取决于慢速/同步函数的性质:
-
它是否是一些相对短暂的同步任务,而您正试图避免 UI 中出现短暂的故障?
在这种情况下,常见的建议通常是“独立”任务:
func asyncGetData() async throws -> Data {
try await Task.detached {
try self.someSyncFunction()
}.value
}
但这是非结构化并发 https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency#Unstructured-Concurrency,因此您自己承担支持任务取消的负担。所以像下面这样的东西可能更合适:
func asyncGetData() async throws -> Data {
let task = Task.detached {
try self.longTimeFunction()
}
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
(不用说,这是假设longTimeFunction
甚至支持取消。下面详细介绍一下。它还假设longTimeFunction
与相关参与者并不孤立;您可能想确保它是非隔离的。)
或者,从 Swift 5.7 开始(感谢SE-0338 https://github.com/apple/swift-evolution/blob/main/proposals/0338-clarify-execution-non-actor-async.md),您可以享受结构化并发,并通过非隔离的方式将其从当前参与者中分离出来async
功能:
nonisolated func asyncGetData() async throws -> Data {
try self.longTimeFunction()
}
因为这是结构化并发,所以我们免费获得取消传播。而且因为它是非隔离的async
函数,它从当前的 actor 中获取它。
为了完整起见,还有其他选择。但这些是从当前演员手中夺取任务的一些常见方法。
-
这个缓慢的函数是否执行一些计算密集型计算?
为了确保演员能够始终取得“进步”,您需要定期Task.yield
。在这种情况下,您还需要确保它定期检查取消情况,并使用checkCancellation https://developer.apple.com/documentation/swift/task/checkcancellation() or isCancelled https://developer.apple.com/documentation/swift/task/iscancelled-swift.type.property, too.
例如:
func longTimeFunction() async throws -> Data {
…
for i in … {
try Task.checkCancellation()
await Task.yield()
…
}
return …
}
现在,您检查取消和收益的频率由您决定。我通常会在计算速度和我想要检查的频率之间取得一些平衡(例如,if i.isMultiple(of: 100) {…}
),但是,希望您能明白这一点。
-
或者它确实是一个缓慢的同步函数,您无法轻松重构以支持 Swift 并发?
然后,我们必须意识到,Swift 并发是基于契约的,以确保任务始终能够取得进展。
具体来说,SE-0296 - 异步/等待 https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md警告我们要么必须提供某种方式在 Swift 并发中交错(例如,使用Task.yield
如前一点所述)或者我们“通常在单独的上下文中运行它”:
由于潜在的挂起点只能出现在异步函数中显式标记的点处,因此长计算仍然会阻塞线程。当调用只执行大量工作的同步函数时,或者遇到直接在异步函数中编写的特别密集的计算循环时,可能会发生这种情况。在任何一种情况下,线程都无法在这些计算运行时交错代码,这通常是正确性的正确选择,但也可能成为可扩展性问题。需要进行密集计算的异步程序通常应该在单独的上下文中运行。
Swift 演进提案没有明确定义“在单独的上下文中运行”。但在 WWDC 2022 视频中可视化和优化 Swift 并发 https://developer.apple.com/videos/play/wwdc2022/110350/?time=1186,他们建议 GCD:
如果您有需要执行这些操作的代码,请将该代码移出并发线程池(例如,通过在调度队列上运行它),并使用延续将其桥接到并发世界。只要有可能,请使用异步 API 进行阻塞操作,以保持系统平稳运行。
简而言之,在这种情况下,您将采用选项 2,将 GCD 代码封装在withCheckedThrowingContinuation
。显然,我们必须担心 GCD 的所有传统问题(例如,减轻线程爆炸、手动确保线程安全、避免死锁等)仍然适用。请注意,当您开始使用 GCD API 占用 CPU 核心时,这超出了 Swift 并发协作线程池的范围,因此您仍然可能会遇到 CPU 过度使用,而该池旨在缓解这一问题。
因此,底线完全取决于同步和/或长时间运行例程的性质。不幸的是,如果没有更多信息,就不可能更具体syncLongTimeFunction
.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)