调查任务中函数调用触发的上下文变化

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

我很好奇为什么Task CTask D不在主线程上执行,与Task ATask B不同。我理解任务从其父级继承上下文,在这种情况下,它们都共享同一个父级。是否可以假设调用函数会导致创建detached任务?

struct ThreadTestView: View {
    var body: some View {
        Text("Test Thread")
            .onAppear {
                // Task A
                print("Task A - \(threadInfo())")
                
                // Task B
                Task {
                    print("Task B - \(threadInfo())")
                }
                
                // Task C
                printInfo("Task C")
                
                // Task D
                Task {
                    printInfo("Task D")
                }
            }
    }
    
    func printInfo(_ source: String) {
        Task {
            print("\(source) - \(threadInfo())")
        }
        // detached
        Task.detached {
            print("\(source) - detached \(threadInfo())")
        }
    }
    
    func threadInfo() -> String {
        "isMain:\(Thread.isMainThread) - \(Thread.current)"
    }
}

#Preview {
    ThreadTestView()
}

控制台输出:

Task A - isMain:true - <_NSMainThread: 0x280694040>{number = 1, name = main}
Task C - isMain:false - <NSThread: 0x2806c6b00>{number = 8, name = (null)}
Task C - detached isMain:false - <NSThread: 0x2806c4580>{number = 5, name = (null)}
Task B - isMain:true - <_NSMainThread: 0x280694040>{number = 1, name = main}
Task D - isMain:false - <NSThread: 0x2806c6b00>{number = 8, name = (null)}
Task D - detached isMain:false - <NSThread: 0x2806c6b00>{number = 8, name = (null)}
swift swiftui async-await concurrency task
3个回答
1
投票

A 和 B 位于主线程上,因为

body
与主要参与者隔离(因此在主线程上运行)。显然 A 将在主线程上运行。 B 启动
Task {…}
非结构化并发,这会在当前参与者(即本例中的主要参与者)上创建一个新的顶级任务,因此它也运行在主线程上。

然而,C 和 D 不在主线程上,因为

printInfo

 不是参与者隔离的,因此,无论您如何启动非结构化并发,生成的任务也不是参与者隔离的。因此,由 
printInfo
 创建的任务在协作线程池中的线程上运行,而不是在主线程上运行。


如果

printInfo

 方法与主要参与者隔离(要么显式地以 
@MainActor
 本身为前缀,或者是与主要参与者及其所有成员隔离的类型的成员),则故事将会改变。然后它的 
Task {…}
 将被隔离到同一个 actor,而 
Task.detached {…}
 仍然不会被隔离到任何 actor,因此将在协作线程池中的随机线程上运行。


0
投票
巨大的免责声明:我仍在填补自己对 Swift Concurrency 理解的空白,因此我很高兴以下任何陈述被证明是错误的。

我认为这里重要的一点是“主线”与“主角”不是一回事。两者之间存在一些差异,但一个显着的差异是参与者是一种编译时构造,而线程主要是一种运行时构造。仅仅因为代码在主线程上运行并不意味着它是“主要参与者隔离的”。查看

onAppear(perform:) 方法的文档,您可以看到 perform

 闭包不包含 
@MainActor
 注释,因此它不是主要参与者隔离的,尽管我们可以非常有信心它将在主线程上调用。因此,当您从该闭包内部实例化 
Task
 时(尽管 
Task.init
 继承了当前 actor),闭包本身并不是与 actor 隔离的,因此没有可以继承的 actor。执行器可以自由地将此任务安排在协作线程池中可用的任何线程上,甚至可以安排在主线程上。尝试用 
printInfo
 注释您的 
@MainActor
 函数,看看情况如何变化。


0
投票
这是我的理解。如果有人对正在发生的事情有更深入的了解,我们将很乐意纠正/改进它。

您的视图 (

struct ThreadTestView: View

) 未注释为 
@MainActor
。协议视图本身也没有用 
@MainActor
 注释。仅
body
内的
protocol View
@MainActor
注释:

@ViewBuilder @MainActor var body: Self.Body { get }


所以我认为这种行为源于这 3 个事实。

首先,我们不仅应该监视

printInfo

 中生成的任务,还应该监视函数本身的调用位置:

func printInfo(_ source: String) { // THIS: where the function itself is called print("\(source) - function itself \(threadInfo())") Task { print("\(source) - \(threadInfo())") } // detached Task.detached { print("\(source) - detached \(threadInfo())") } }
那么我在这里期待什么:

在所有 4 种情况下,
    函数
  • printInfo
     本身(我在上面添加的新行)都将在主线程上调用。这是因为所有以 
    body
     作为父项(用 
    @MainActor
     注释)的项目都将保留该上下文。
  • 在情况 C 中,任务是从
  • printInfo
     生成的,正如我们所建立的,它不是 @MainActor。因此,该任务也不是主要参与者。
  • 在情况 D 中,第一个任务是从
  • body
     内部生成的,它将继承主参与者上下文(因此函数 
    printBody
     将在主线程上运行) - 与情况 B 非常相似。但是第二个任务是从该函数生成的
    printInfo
     不会出现在主线程上,就像情况 C 一样。
我同意案例 C 和 D 令人困惑。关于这些限制的一些很好的讨论

这里这里

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