SWIFT 任务延续误用: - 方法泄露了其延续! - 不阻塞主线程?

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

我必须将

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

ios swift multithreading async-await
1个回答
0
投票

我的第一个问题是,如果后面的代码从未执行过,为什么我仍然可以使用主线程? 主线程不应该被阻塞在那里等待完成吗?

这不是 Swift Concurrency 的工作原理。 Swift Concurrency 背后的整个概念/想法是当前线程不会被

await
调用阻塞。

简单来说,您可以将异步操作想象为在线程内部处理的一项工作。但是,在等待操作时,可以执行其他操作,因为您只是创建一个挂起点。异步操作的整个管理和处理都是由 Swift 内部处理的。

一般来说,使用 Swift Concurrency,您应该避免考虑“线程”,这些是在内部管理的,并且执行操作的线程故意对外界不可见。

事实上,使用 Swift Concurrency,你甚至不允许阻塞线程,但这是另一个话题了。

如果您想了解有关 async/await 以及 Swift 实现的概念的更多详细信息,我建议您阅读 SE-0296 或观看 Apple 就该主题发布的众多 WWDC 视频之一。

我的第二个问题是,系统如何知道永远不会调用完成? 它是否跟踪完成是否在运行时保留在外部库中,如果引用的所有者死亡,它会引发此泄漏警告?

参见官方文档

错过调用它(最终)将导致调用任务无限期地保持挂起状态,这将导致任务“挂起”并被泄漏而无法销毁它。

检查的延续提供了误用检测,并且删除对它的最后一个引用而不恢复它会触发警告。恢复连续两次也会被诊断出来并会导致崩溃。

对于您的其余问题,我假设您已向我们展示了代码的所有相关部分。

我的第三个问题是,这会导致崩溃吗?或者系统只是取消任务而我不应该担心?

只有多次调用延续才会导致崩溃(请参阅我之前的答案)。 但是,您绝对应该确保调用延续,否则您将创建一个永远无法解决的挂起点。把它想象成一个永远不会完成并因此导致泄漏的操作。

我的最后一个问题是,如果我无法修改外部库,我该怎么办?

根据您向我们展示的代码,实际上只有一种可能性:

多次调用

doSomething
会导致对仍在运行的同一方法的调用被库内部取消,因此永远不会调用完成闭包。

因此,您应该检查

doSomething
的文档,了解其关于多次通话和取消的说明。

如果图书馆没有为您提供检测取消的方法,您可以做什么:

  1. 防止多次调用该方法。这可能是最简单的解决方案,例如,您可以在库方法仍在运行时简单地停用按钮。
  2. 如果您想故意允许多个调用,则必须检测这些情况并完成任何未完成的延续。

这是一个非常简单的代码示例,应该演示如何解决这种情况下的问题:

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
永远不会调用其完成处理程序的其他场景。

© www.soinside.com 2019 - 2024. All rights reserved.