SWIFT 任务延续误用:泄露了其延续 - 对于代表?

问题描述 投票:0回答:2

我正在尝试使用

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)
}

我做错了什么或者有更好的方法来实现这一目标吗?

swift concurrency
2个回答
46
投票

如果您通过

withCheckedContinuation
withCheckedThrowingContinuation
创建的延续在被丢弃之前未报告成功或失败,则会出现此消息。这会导致资源泄漏:

多次从延续中恢复是未定义的行为。 永不恢复会使任务无限期地处于挂起状态,并泄漏任何关联的资源。如果违反这些不变量中的任何一个,CheckedContinuation 会记录一条消息。

摘自CheckedContinuation

文档
(强调我的)。

问题不仅仅是存在一些资源泄漏,永远不恢复延续意味着某些

await
调用将永远挂起,这取决于您的项目架构,这可能或多或少有问题。


发生这种情况的可能原因如下:

  1. 并非所有代码路径都会恢复延续,例如有一个
    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)
            })
        }
    }
}
  1. “旧”风格的异步函数不会在所有路径上调用完成闭包;这是一个不太明显的原因,有时更难调试:
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
    }
}
  1. 当调用函数时,延续被存储为属性;这意味着如果在第一个函数调用正在进行时发生第二个函数调用,则第一个完成将被覆盖,这意味着它永远不会报告完成:
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() 
}

我还强烈建议将您的类转换为演员,以避免数据竞争,无论您是否像现在一样存储一个延续,或者使用数组。原因是连续属性是从多个线程消耗的,在某些时候您可能最终会出现两个线程同时访问/写入该属性。


-1
投票

我认为问题就在这里 -

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)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.