I'm curious about why Task C and Task D are not executed on the main thread, unlike Task A and Task B. I comprehend that tasks inherit context from their parent, and in this case, they all share the same parent. Could it be assumed that the calling function results in the creation of detached tasks?
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()
}
console output:
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)}
Here's my understanding. Will be happy to correct/improve it if anyone has a deeper understanding of what's going on.
Your View (struct ThreadTestView: View
) is not annotated as @MainActor
. Protocol View itself is not annotated with @MainActor
either. Only body
inside protocol View
is annotated with @MainActor
:
@ViewBuilder @MainActor var body: Self.Body { get }
So I think the behavior derives from those 3 facts.
First of all we should monitor not only tasks spawned in printInfo
, but also where the function itself is called:
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())")
}
}
So what do I expect here:
printInfo
itself (the new line I added above) will be called on main thread in all 4 cases. This is because all the items that have body
as a parent (which is annotated with @MainActor
) will keep that context.printInfo
, which is not, as we established, a @MainActor. Hence that Task is not a main actor either.body
, and it will inherit the main actor context (so function printBody
will run on main thread) - much like case B. But the second Task is spawned from the function printInfo
will not be on main thread, like case C.I agree that cases C and D are confusing. Some good discussion about these limitations here and here