我必须将
async/await
与 withCheckedThrowingContinuation
一起使用才能从外部库获取结果(我无法修改此库)。
我从 UIViewController
称呼它(如果我理解正确的话,Task
将在 MainActor
上)。
但是对外部lib的调用有时会打印
SWIFT TASK CONTINUATION MISUSE: leaked its continuation
,这意味着如果我理解正确的话,它的完成将不会被调用,并且这之后的代码不会被执行。尤其是当我向按钮发送垃圾邮件时,就会发生这种情况。
我的第一个问题是,如果后面的代码从未执行过,为什么我仍然可以使用主线程?主线程不应该被阻塞在那里等待完成吗?
我的第二个问题是,系统如何知道永远不会调用完成?它是否跟踪完成是否在运行时保留在外部库中,如果引用的所有者死亡,它会引发此泄漏警告?
我的第三个问题是,这会导致崩溃吗?或者系统只是取消任务而我不应该担心?
我的最后一个问题是,如果我无法修改外部库,我该怎么办?
这是我的代码示例:
class MyViewController: UIViewController {
func onButtonTap() {
Task {
do {
try await callExternalLib() // <--- This can be called after the leak
}
}
}
func callExternalLib() async throws {
print("Main thread") // <--- This is always called on the main thread
try await withCheckedThrowingContinuation { continuation in
myExternalLib.doSomething {
continuation.resume()
}, { error in
continuation.resume(throwing: error)
}
}
print("Here thread is unlocked") // <--- This is never called when it leaks
}
}
我的第一个问题是,如果后面的代码从未执行过,为什么我仍然可以使用主线程? 主线程不应该被阻塞在那里等待完成吗?
这不是 Swift Concurrency 的工作原理。 Swift Concurrency 背后的整个概念/想法是当前线程不会被
await
调用阻塞。
简单来说,您可以将异步操作想象为在线程内部处理的一项工作。但是,在等待操作时,可以执行其他操作,因为您只是创建一个挂起点。异步操作的整个管理和处理都是由 Swift 内部处理的。
一般来说,使用 Swift Concurrency,您应该避免考虑“线程”,这些是在内部管理的,并且执行操作的线程故意对外界不可见。
事实上,使用 Swift Concurrency,你甚至不允许阻塞线程,但这是另一个话题了。
如果您想了解有关 async/await 以及 Swift 实现的概念的更多详细信息,我建议您阅读 SE-0296 或观看 Apple 就该主题发布的众多 WWDC 视频之一。
我的第二个问题是,系统如何知道永远不会调用完成? 它是否跟踪完成是否在运行时保留在外部库中,如果引用的所有者死亡,它会引发此泄漏警告?
参见官方文档:
错过调用它(最终)将导致调用任务无限期地保持挂起状态,这将导致任务“挂起”并被泄漏而无法销毁它。
检查的延续提供了误用检测,并且删除对它的最后一个引用而不恢复它会触发警告。恢复连续两次也会被诊断出来并会导致崩溃。
对于您的其余问题,我假设您已向我们展示了代码的所有相关部分。
我的第三个问题是,这会导致崩溃吗?或者系统只是取消任务而我不应该担心?
只有多次调用延续才会导致崩溃(请参阅我之前的答案)。 但是,您绝对应该确保调用延续,否则您将创建一个永远无法解决的挂起点。把它想象成一个永远不会完成并因此导致泄漏的操作。
我的最后一个问题是,如果我无法修改外部库,我该怎么办?
根据您向我们展示的代码,实际上只有一种可能性:
多次调用
doSomething
会导致对仍在运行的同一方法的调用被库内部取消,因此永远不会调用完成闭包。
因此,您应该检查
doSomething
的文档,了解其关于多次通话和取消的说明。
如果图书馆没有为您提供检测取消的方法,您可以做什么:
这是一个非常简单的代码示例,应该演示如何解决这种情况下的问题:
private var pendingContinuation: (UUID, CheckedContinuation<Void, any Error>)?
func callExternalLib() async throws {
if let (_, continuation) = pendingContinuation {
print("Cancelling pending continuation")
continuation.resume(throwing: CancellationError())
self.pendingContinuation = nil
}
try await withCheckedThrowingContinuation { continuation in
let continuationID = UUID()
pendingContinuation = (continuationID, continuation)
myExternalLib.doSomething {
Task { @MainActor in
if let (id, continuation) = self.pendingContinuation, id == continuationID {
self.pendingContinuation = nil
continuation.resume()
}
}
} error: { error in
Task { @MainActor in
if let (id, continuation) = self.pendingContinuation, id == continuationID {
self.pendingContinuation = nil
continuation.resume(throwing: error)
}
}
}
}
}
请注意,此解决方案假设不存在
doSomething
永远不会调用其完成处理程序的其他场景。