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

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

SwiftUI 有 this

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

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

swift swiftui concurrency
2个回答
12
投票

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

enter image description here

这是有道理的,因为

body
与主要演员隔离,我希望它的
task
也代表当前演员(主要演员)运行。 (如果你需要从当前的 actor 中获取一些东西,你通常会使用 detached 任务。不过,你只需要对缓慢、同步的工作单元执行此操作。)FWIW,当代版本的 SwiftUI 现在隔离了整个
 View
给主要演员,进一步减少任何歧义。

底线,

.task
的 SwiftUI
body
视图修改器与主要参与者隔离。 (FWIW,我也通过多种方式凭经验验证了这一点。)

简而言之,如果您确实需要确保某些内容脱离主要参与者(例如,在

.task
视图修改器中缓慢且同步地运行某些内容),请确保将其放在独立任务中的单独参与者上,或者一些等效的模式。


其他一些观察:

  1. 顺便说一句,我们不再关注 Swift 并发中的线程。它可能会产生误导性的结果。 Swift 并发使我们脱离了线程级推理。将来,Swift 6 将删除许多我们用来以编程方式查询

    Thread
    信息的 API。最重要的是,停止担心线程并专注于参与者隔离。

    如果您确实想了解有关 Swift 并发线程模型的更多信息,请参阅 WWDC 2021 视频Swift 并发:幕后花絮。它不会直接涉及

    .task
    视图修饰符问题,但它将帮助您理解为什么我们不再像以前那样关注线程。

  2. 就我个人而言,在我的

    .task
    视图修饰符中,我通常只是调用一些
    async
    方法,在这种情况下,
    .task
    视图修饰符使用的 actor 基本上变得无关紧要(因为我们只是
    await
    调用,它会暂停当前 Actor 的执行,将其释放以去做其他事情)。

  3. 同样,回到 GCD 世界,我们浪费了大量的精力来担心我们是否从正确的队列中调用了某些特定的方法(如果我们搞砸了,就要处理运行时错误)。但在 Swift 并发中,我们将被调用的函数隔离到适当的参与者,现在,如果我们没有正确调用它,我们会收到编译时警告(例如,当我们使用“严格并发检查”构建设置时,双重检查

    Sendable
    类型) “完整”,当我们跨越参与者边界时忽略包含所需的
    await
    ,等等)。


在评论中,您提到您正在“检查线程……在任务修改器[调用]的

async
函数内”。

是的,SE-0338,在 Swift 5.7 中实现,告诉我们非隔离的

async
函数“不在任何 actor 的执行器上运行”。因此,除非它是一个以某种方式明确与主要参与者隔离的函数,否则它不会在主要参与者上运行。


2
投票

您必须以不同的方式考虑线程与 async/await 和

.task
修饰符。尽管它从主线程开始,但每一行等待代码都会执行一些操作,当它完成时,它会移至下一行。等待的行运行的实际线程取决于其参与者。如果它是
View
结构内的异步函数,那么它是主线程,因为它被标记为
@MainActor
,因此主线程将在等待行期间被阻塞。如果它在不同的结构或类中声明,那么它应该是随机后台线程。您还可以通过使用子项
.task
Task { }
中获取后台线程,即使显示此 View 结构的屏幕消失,该子项
Task
仍然会以相同的方式取消
.task
,请小心不要使用
Task.detatched
因为这不会被取消并可能导致崩溃。在
View
函数中获取后台线程的另一种方法是将其标记为
nonisolated
。在该函数内放置一个断点以检查它是否在后台线程上。一些在后台线程中执行异步函数的类或参与者使用
MainActor.run
Task { @MainActor in
切换到主线程,例如更新
@Published
。并不是我们不再需要 Xcode 15/iOS 17 中新的
@Observable
fetcher 类,例如

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