11

我正在尝试新的 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说我主线程上。这不是我所期望的。

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

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

(在某种程度上,我可以通过在调用Task.detached之后再次调用来“解决”这个问题getBounds;但那时我的代码看起来非常像嵌套的 GCD,我不得不怀疑我为什么要使用 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

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

4

4 回答 4

4

据我了解,鉴于这都是非常新的,不能保证asyncDetached必须在主线程之外调度。

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

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

于 2021-06-11T16:21:43.127 回答
2

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

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.boundswazoo的调用,view访问和bounds访问都在主线程上。此处的nonisolated指定对于确保这一点至关重要。对此的需求和随之而来的需求让await我感到非常惊讶,但这一切似乎都与演员的性质以及 UIViewController 是 MainActor 的事实有关。

于 2021-06-13T21:48:30.493 回答
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 另一个机会,让你的工作脱离主线程。有关更多详细信息,请参阅此处的自愿暂停Task.yield()

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

编辑:

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

于 2021-06-11T20:39:38.560 回答
0

我有一个运行长任务并且应该始终在后台线程上运行的函数的相同问题。最终使用新的 Swift 异步等待语法,我找不到基于 Task 或 TaskGroup 的简单方法来确保这一点。Task.detached 调用将使用不同的线程,Task 调用本身也是如此。如果这是从 MainThread 调用的,它将是另一个线程,如果不是,它可以是主线程。最终我找到了一个总是有效的解决方案——但看起来很“hacky”。

  1. 通过成为 MainActor 隔离类(如视图控制器)的一部分,确保您的后台函数未与主线程隔离

    nonisolated func iAllwaysRunOnBackground() {}
    
  2. 测试主线程,如果在主线程上执行,在Task中再次调用该函数,等待执行并返回

    nonisolated func iAllwaysRunOnBackground() async throws {
      if Thread.isMainThread {
         async let newTask = Task {
           try await self.iAllwaysRunOnBackground()
         }
        let _ = try await newTask.value
        return 
      }
    
      function body
    }
    
于 2022-02-22T13:03:02.763 回答