【问题标题】:asyncDetached falling back into main thread after MainActor call在 MainActor 调用后,asyncDetached 回到主线程
【发布时间】:2021-08-27 00:36:03
【问题描述】:

我正在尝试新的 async/await 东西。我这里的目标是在后台运行test()方法,所以我使用Task.detached;但是在test() 期间,我需要在主线程上进行调用,所以我使用的是 MainActor。

(我意识到这在孤立的情况下可能看起来很复杂,但它从一个更好的真实案例中减少了。)

好的,测试代码如下所示(在视图控制器中):

override func viewDidLoad() {
    super.viewDidLoad()
    Task.detached(priority: .userInitiated) {
        await self.test()
    }
}
@MainActor func getBounds() async -> CGRect {
    let bounds = self.view.bounds
    return bounds
}
func test() async {
    print("test 1", Thread.isMainThread) // false
    let bounds = await self.getBounds()
    print("test 2", Thread.isMainThread) // true
}

第一个print 说我不在主线程上。这正是我所期望的。

但是第二个print 说我在主线程上am。这不是我所期望的。

感觉好像我只是因为调用了一个 MainActor 函数而神秘地回到了主线程。我以为我会等待主线程,然后在我已经在的后台线程中恢复。

这是一个错误,还是我的期望有误?如果是后者,在await 期间我如何到主线程,然后又回到我所在的线程?我认为这正是 async/await 可以轻松实现的功能...?

(在某种程度上,我可以通过在调用getBounds 之后再次调用Task.detached 来“解决”这个问题;但那时我的代码看起来非常像嵌套的 GCD,我不得不想知道为什么我' m 完全使用 async/await。)

也许我还为时过早,但我继续将此作为错误提交:https://bugs.swift.org/browse/SR-14756


更多注释

我可以通过替换来解决问题

    let bounds = await self.getBounds()

    async let bounds = self.getBounds()
    let thebounds = await bounds

但这似乎是不必要的复杂,并不能说服我原来的现象不是错误。


我也可以通过使用演员来解决问题,这开始看起来是最好的方法。但同样,这并不能让我相信我在这里注意到的现象不是错误。


我越来越确信这是一个错误。我刚刚遇到(并报告)以下内容:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    async {
        print("howdy")
        await doSomeNetworking()
    }
}
func doSomeNetworking() async {
    print(Thread.isMainThread)
}

这会打印howdy,然后第二个print 会打印true。但是如果我们注释掉第一个打印,剩下的(第二个)print 打印false

仅仅添加或删除一条打印语句如何改变我们所在的线程?这肯定不是故意的。

【问题讨论】:

  • 以防万一您认为Thread.isMainThread 以某种方式弄错了,事实并非如此。我知道,因为如果我在await 之前在test 中谈到self.view.bounds,我会在主线程检查器中崩溃,但如果我在await 之后谈到它,我不会崩溃。我们这里确实是上下文切换,我不知道为什么。
  • 如果你调用另一个不是 MainActor 的异步方法会发生什么?
  • @EmilioPelaez 如果这样做会怎样?
  • 我在这里注意到的一件事是.userInitiated 优先级,这对于昂贵的工作来说是非常高的。这可能会影响一些调度决策。也许你应该在做昂贵的工作之前再做一个优先级较低的asyncDetached
  • 就像detachedAsyncasync 一样,优先级与我所描述的结果无关。自己试试吧。

标签: swift async-await swift5.5 mainactor


【解决方案1】:

以下公式有效,并且非常优雅地解决了整个问题,尽管我有点不愿意发布它,因为我不太了解它是如何工作的:

override func viewDidLoad() {
    super.viewDidLoad()
    Task {
        await self.test2()
    }
}
nonisolated func test2() async {
    print("test 1", Thread.isMainThread) // false
    let bounds = await self.view.bounds // access on main thread!
    print("test 2", bounds, Thread.isMainThread) // false
}

我已经测试了await self.view.bounds 调用wazoo,view 访问和bounds 访问都在主线程上。这里的nonisolated 名称对于确保这一点至关重要。对此的需求以及对await 的伴随需求让我感到非常惊讶,但这似乎都与演员的性质以及 UIViewController 是 MainActor 的事实有关。

【讨论】:

    【解决方案2】:

    即使您进行测试以找出等待@MainActor 函数的确切线程行为,您也不应该依赖它。正如@fullsailor 的回答一样,该语言明确不保证在等待后将在同一个线程上恢复工作,因此这种行为可能会在任何操作系统更新中发生变化。将来,您可能可以使用自定义执行程序请求特定线程,但这目前不在该语言中。详情请见https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md

    此外,希望它不会导致您在主线程上运行的任何问题。有关调度如何工作的详细信息,请参阅https://developer.apple.com/videos/play/wwdc2021/10254/?time=2074。你不应该害怕通过调用@MainActor 函数来阻塞主线程,然后再做昂贵的工作:如果有更重要的 UI 工作可用,这将安排在你的工作之前,或者你的工作将在另一个线程上运行.如果你特别担心,你可以在你昂贵的工作之前使用Task.yield(),给 Swift 另一个机会,让你的工作脱离主线程。请参阅自愿暂停here,了解有关Task.yield() 的更多详细信息。

    在您的示例中,Swift 很可能认为不值得从主线程返回进行上下文切换,因为它已经存在,但是如果主线程更加饱和,您可能会遇到不同的行为。

    编辑:

    您在async let 中看到的行为是因为这会产生一个与您正在执行的工作同时运行的子任务。因此,由于那个孩子在主线程上运行,你的其他代码不是。有关子任务的更多详细信息,请参阅https://forums.swift.org/t/concurrency-structured-concurrency/41622

    【讨论】:

    • 我不买这个。我在这里展示的内容大大减少了我需要执行耗时计算的现实世界问题。它绝对不能在主线程上运行;如果是这样,它会阻止接口。这正是由于主线程的这种意外泄漏或上下文切换回我的后台线程而发生的情况。说另一个线程可能会在暂停后接手工作是一回事。没关系。另一种说法是该线程将突然成为 main 线程。
    • 到目前为止,我已经为这个问题找到了两种解决方法,但在我看来,这并不能减少我所展示的问题。
    • 有趣!我很高兴看到您的解决方案:我的解决方案是将 Task.yield() 放在昂贵的工作上,以让其他主线程工作进入。或者甚至可能是 if Thread.isMainThread { Task.yield() } 以避免拆分您的工作,除非您在主线程上线。需要明确的是,上下文切换可能不是我们应该用来快速讨论async 函数的语言,因为延续是在堆上的线程之间传递的,这不涉及完整的上下文切换,包括线程局部变量等.无论哪种方式,您都可能有自定义执行器的有效用例!
    • 这将完全违背Tasks 的目的,因为将内容移出主线程是您首先想要做的。
    【解决方案3】:

    据我了解,鉴于这一切都是新事物,因此无法保证 asyncDetached 必须调度主线程。

    Swift Concurrency: Behind the Scenes 会话中,讨论了调度程序将尝试将事物保持在同一个线程上以避免上下文切换。不过,鉴于此,我不知道您将如何专门避开主线程,但也许我们不应该关心,只要任务取得进展并且永不阻塞。

    我找到了时间戳 (23:18),它解释了不能保证同一个线程会在等待之后继续执行。 https://developer.apple.com/videos/play/wwdc2021/10254/?time=1398

    【讨论】:

    • asyncDetached 的使用在这里并不重要; async 可以。但是由于主线程泄漏到任务中,任务(在现实世界中)确实阻塞。我不在乎是否有另一个线程接手工作;谁在乎?但它应该是 main 线程,在我的计算过程中阻止用户交互 30 秒,这是令人发指的。
    猜你喜欢
    • 1970-01-01
    • 2020-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多