我正在尝试使用
class
功能扩展我的 async/await
,但在运行时控制台中出现错误:
SWIFT TASK CONTINUATION MISUSE: query(_:) leaked its continuation!
下面是
class
我正在尝试添加使用委托的延续:
class LocalSearch: NSObject, MKLocalSearchCompleterDelegate {
private let completer: MKLocalSearchCompleter
private var completionContinuation: CheckedContinuation<[MKLocalSearchCompletion], Error>?
init() {
completer = MKLocalSearchCompleter()
super.init()
completer.delegate = self
}
func query(_ value: String) async throws -> [MKLocalSearchCompletion] {
try await withCheckedThrowingContinuation { continuation in
completionContinuation = continuation
guard !value.isEmpty else {
completionContinuation?.resume(returning: [])
completionContinuation = nil
return
}
completer.queryFragment = value
}
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
completionContinuation?.resume(returning: completer.results)
completionContinuation = nil
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
completionContinuation?.resume(throwing: error)
completionContinuation = nil
}
}
这就是我的使用方式:
let localSearch = LocalSearch()
do {
let results = try await localSearch.query("toront")
print(results)
} catch {
print(error)
}
我做错了什么或者有更好的方法来实现这一目标吗?
如果您通过
withCheckedContinuation
或 withCheckedThrowingContinuation
创建的延续在被丢弃之前未报告成功或失败,则会出现此消息。这会导致资源泄漏:
多次从延续中恢复是未定义的行为。 永不恢复会使任务无限期地处于挂起状态,并泄漏任何关联的资源。如果违反这些不变量中的任何一个,CheckedContinuation 会记录一条消息。
文档(强调我的)。
问题不仅仅是存在一些资源泄漏,永远不恢复延续意味着某些
await
调用将永远挂起,这取决于您的项目架构,这可能或多或少有问题。
发生这种情况的可能原因如下:
if
/guard
/case
退出范围而不指示继续报告成功/失败class Searcher {
func search(for query: String) async throws -> [String] {
await withCheckedContinuation { continuation in
someFunctionCall(withCompletion: { [weak self] in
guard let `self` = self else {
// if `result` doesn't have the expected value, the continuation
// will never report completion
return
}
continuation.resume(returning: something)
})
}
}
}
class Searcher {
private let internalSearcher = InternalSearcher()
func search(for query: String) async throws -> [String] {
await withCheckedContinuation { continuation in
internalSearcher.search(query: query) { result in
// everything fine here
continuation.resume(returning: result)
}
}
}
}
class InternalSearcher {
func search(query: String, completion: @escaping ([String]) -> Void {
guard !query.isEmpty else {
return
// legit precondition check, however in this case,
// the completion is not called, meaning that the
// upstream function call will imediately discard
// the continuation, without instructing it to report completion
}
// perform the actual search, report the results
}
}
class Searcher {
var continuation: CheckedContinuation<[String], Error>?
func search(for query: String) async throws -> [String] {
try await withCheckedTrowingContinuation { continuation in
// note how a second call to `search` will overwrite the
// previous continuation, in case the delegate method was
// not yet called
self.continuation = continuation
// trigger the searching mechanism
}
}
func delegateMethod(results: [String]) {
self.continuation.resume(returning: results)
self.continuation = nil
}
}
#1 和 #2 通常发生在处理接受完成回调的函数时,而 #3 通常发生在处理委托方法时,因为在这种情况下,我们需要将延续存储在异步函数范围之外的某个地方,以便访问它来自委托方法。
底线 - 尝试确保延续在所有可能的代码路径上报告完成,否则,延续将无限期地阻止异步调用,导致与该异步调用关联的任务泄漏其关联资源。
在您的情况下,可能发生的情况是在第一个呼叫有机会完成之前发生了第二个
query()
呼叫。
在这种情况下,第一个延续被丢弃而没有报告完成,这意味着第一个调用者在
try await query()
调用之后从未继续执行,这根本不行。
需要修复以下代码,以免覆盖挂起的延续:
func query(_ value: String) async throws -> [MKLocalSearchCompletion] {
try await withCheckedThrowingContinuation { continuation in
completionContinuation = continuation
一个快速的解决方案是存储一个延续数组,恢复委托方法中的所有延续,然后清除数组。此外,在您的特定情况下,您可以简单地从延续代码中提取验证,因为即使在异步函数中,您也可以同步返回/抛出:
func query(_ value: String) async throws -> [MKLocalSearchCompletion] {
guard !value.isEmpty else {
return []
}
return try await withCheckedThrowingContinuation { continuation in
continuations.append(continuation)
completer.queryFragment = value
}
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
continuations.forEach { $0.resume(returning: completer.results) }
continuations.removeAll()
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
continuations.forEach { $0.resume(throwing: error) }
continuations.removeAll()
}
我还强烈建议将您的类转换为演员,以避免数据竞争,无论您是否像现在一样存储一个延续,或者使用数组。原因是连续属性是从多个线程消耗的,在某些时候您可能最终会出现两个线程同时访问/写入该属性。
我认为问题就在这里 -
func query(_ value: String) async throws -> [MKLocalSearchCompletion] {
try await withCheckedThrowingContinuation { continuation in
// storing into a variable makes this continuation instance outlive the scope of it
// In other words, it leaks OR escapes the scope
// This is same as why we need to add @escaping attribute for callback functions arguments
// those are either stored in variables like this
// or passed to other functions (escaping scope of current function)
completionContinuation = continuation
// Try commenting above line, the warning should go away
// And your code will stop working as well :)
// How to design this component is other question.
}
}
import MapKit
class LocalSearch: NSObject, MKLocalSearchCompleterDelegate {
typealias Completion = (_ results: [MKLocalSearchCompletion]?, _ error: Error?) -> Void
private let completer: MKLocalSearchCompleter
private var completion: Completion?
override init() {
completer = MKLocalSearchCompleter()
super.init()
completer.delegate = self
}
func query(_ value: String, completion: @escaping Completion) {
self.completion = completion
completer.queryFragment = value
}
func query(_ value: String) async throws -> [MKLocalSearchCompletion] {
try await withCheckedThrowingContinuation { continuation in
guard !value.isEmpty else {
continuation.resume(returning: [])
return
}
self.query(value, completion: { (results, error) in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: results ?? [])
}
})
}
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
completion?(completer.results, nil)
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
completion?(nil, error)
}
}