SwiftUI .task视图修饰符:它在哪个线程上运行?

huangapple go评论79阅读模式
英文:

SwiftUI .task view modifier: in which thread does it run?

问题

SwiftUI拥有.task(priority:_:)视图修饰符。它运行异步代码。默认优先级为userInitiated。没有提到它运行在哪个线程上。

仅通过测试,似乎运行在后台线程,但是:可以安全地假设它始终在非主线程上运行吗?

英文:

SwiftUI has this .task(priority:_:) view modifier. It runs async code. The default priority is userInitiated. There are no mentions to which thread this runs on.

Just by testing seems that runs on background threads, but: is it safe to assume that always runs in a non-main thread?

答案1

得分: 4

我不确定你是如何得出这不是在主线程上运行的结论的,但当我在调试器上运行它时,它确实在主队列上运行:

SwiftUI .task视图修饰符:它在哪个线程上运行?

这是合理的,因为body 是隔离到主要角色的,我期望它的task也代表当前角色(主要角色)运行。 (如果你需要从当前角色中取出一些东西,通常会使用 detached 任务。不过,这仅适用于慢速的同步工作单元。)

总之,SwiftUI 的 .task 视图修饰符对于 body 是隔离到主要角色的。(顺便说一句,我还以多种方式经验证实了这一点。)

简而言之,如果你真的需要确保某些东西不在主要角色上运行(例如,在 .task 视图修饰符中运行某些慢速和同步的东西),请确保将其放在一个单独的角色内,在一个分离的任务内,或一些等效的模式中。


还有一些其他观察:

  1. 顺便说一句,在 Swift 并发中,我们不再关注线程。这可能会导致误导性的结果。Swift 并发将我们抽象出线程级别的推理。在将来,Swift 6 将删除我们曾经用来以编程方式查询 Thread 信息的许多 API。总之,不要再担心线程,关注角色隔离。

    如果你真的想了解更多关于 Swift 并发线程模型的知识,请参阅 WWDC 2021 视频 Swift concurrency: Behind the scenes。它虽然不直接涉及 .task 视图修饰符的问题,但它将帮助你明白为什么我们不再关注线程,就像我们曾经关注过的那样。

  2. 就我个人而言,在我的 .task 视图修饰符中,我通常只是调用一些 async 方法,而在这种情况下,.task 视图修饰符使用的角色变得不太重要(因为我们只是 await 那个调用,它会暂停在当前角色上的执行,使其可以去做其他事情)。

  3. 同样,在 GCD 的世界中,我们浪费了很多精力来担心我们是否从正确的队列中调用了某个特定的方法(如果我们弄错了,会处理运行时错误)。但在 Swift 并发中,我们将被调用的函数隔离到适当的角色,并且如果我们没有正确调用它(例如,跨角色边界时没有双重检查 Sendable 类型,等等),现在会得到编译时警告。


在评论中,你提到你是“在任务修饰符中从异步函数中检查线程”。

是的,SE-0338,在 Swift 5.7 中实施,告诉我们非隔离的 async 函数“不在任何角色的执行器上运行”。因此,除非某个函数以某种方式显式隔离到主要角色,否则它不会在主要角色上运行。

英文:

I am not sure how you concluded that this was not on the main thread, but when I run it on a debugger, it is on the main queue:

SwiftUI .task视图修饰符:它在哪个线程上运行?

And that makes sense, as body is isolated to the main actor, and I would expect its task to run on behalf of the current actor (the main actor), too. (If you need to get something off the current actor, you would generally use a detached task. You only need to do that for slow, synchronous units of work, though.)

Bottom line, the SwiftUI .task view modifier for body is isolated to the main actor. (FWIW, I verified this empirically in a number of ways, too.)

In short, if you really need to make sure something is off the main actor (e.g., running something slow and synchronous within the .task view modifier), make sure to put that on a separate actor, within a detached task, or some equivalent pattern.


A few other observations:

  1. As an aside, we no longer focus on threads within Swift concurrency. It can yield misleading results. Swift concurrency abstracts us away from thread-level reasoning. In the future, Swift 6 will remove many of these API that we used to programmatically query Thread information. Bottom line, stop worrying about threads and focus on actor isolation.

    If you really want to learn more about the Swift concurrency threading model, see WWDC 2021 video Swift concurrency: Behind the scenes. It does not directly touch upon the .task view modifier question, but it will help you appreciate why we no longer focus on threads, like we once may have.

  2. Personally, in my .task view modifiers, I am generally just calling some async method, and in that scenario, the actor used by the .task view modifier becomes largely irrelevant (because we just await that call, which suspends execution on the current actor, freeing it to go do other stuff).

  3. Likewise, back in the GCD world, we wasted a lot of mental energy worrying if we were calling some particular method from the right queue (and dealt with runtime errors if we messed up). But in Swift concurrency, we isolate the called function to the appropriate actor and now we get compile-time warnings if we do not call it correctly (e.g., double-checking Sendable types when we use the “Strict concurrency checking” build setting of “Complete”, neglect to include a needed await when we cross actor boundaries, etc.).


In the comments, you mentioned that you were “checking the thread … inside the async function [called] from the task modifier”.

Yep, SE-0338, implemented in Swift 5.7, informs us that a non-isolated async functions “do not run on any actor’s executor”. So, unless it was a function somehow explicitly isolated to the main actor, it would not run on the main actor.

答案2

得分: 2

你需要以异步/等待和.task修饰符的方式来重新考虑线程处理方式。尽管它始于主线程,但每一行等待代码都会执行某些操作,当它完成时,会继续执行下一行。等待行实际运行的线程取决于其actor。如果它是在View结构体内部的异步函数,那么它是主线程,因为它标记为@MainActor,因此在等待行期间主线程将被阻塞。如果它在不同的结构体或类中声明,那么它应该是一个随机的后台线程。您还可以通过使用子Task { }.task中获得后台线程,即使显示此View结构体的屏幕消失,这个子Task仍然会像.task一样被取消,小心不要使用Task.detached,因为它不会被取消并可能导致崩溃。在View函数中获取后台线程的另一种方法是将其标记为nonisolated。在此函数内部设置断点以检查它是否在后台线程上运行。一些执行其异步函数在后台线程中的类或actor使用MainActor.runTask { @MainActor in来切换到主线程,例如更新@Published。现在在Xcode 15/iOS 17中的新@Observable获取类中,不再需要这样做了。

@Observable
class Test {
    var counter = 0
    
    func update() async {
        do {
            try await Task.sleep(for: .seconds(3))
            counter += 1 // 不再需要将这个包装在Task.mainActor { }中来设置它
        }
        catch {
            
        }
    }
}
英文:

You have to think about threading differently with async/await and the .task modifier. Although it begins on the main thread, each line of await code goes off and does something and when it completes it moves on to the next line. The actual thread that the awaited line runs on depends on its actor. If it is an async func inside the View struct then it is the main thread because that is marked with @MainActor, thus the main thread will be blocked during the await line. If it is declared in a different struct or class then it should be a random background thread. You can also get a background thread within the .task by using a child Task { } and even if the screen that is showing this View struct dissapears this child Task will still be cancelled same way .task is, be careful not to use Task.detatched because that will not be cancelled and could cause a crash. Another way to get a background thread in a View func is to mark it as nonisolated. Put a breakpoint inside this func to check it is on a background thread. Some classes or actors that do their async funcs in background threads use MainActor.run or Task { @MainActor in to switch to the main thread, e.g. to update an @Published. Not we don't need that any more with the new @Observable fetcher classes in Xcode 15/iOS 17, e.g.

@Observable
class Test {
    var counter = 0
    
    func update() async {
        do {
            try await Task.sleep(for: .seconds(3))
            counter+=1 // no longer need to wrap this in Task.mainActor { } to set this anymore
        }
        catch {
            
        }
    }
}

huangapple
  • 本文由 发表于 2023年6月12日 22:20:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76457572.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定