Reputation: 121
Consider the following, relatively simple Swift program:
import Foundation
func printContext(function: String = #function, line: Int = #line) {
print("At \(function):\(line): Running on \(Thread.current) (main: \(Thread.isMainThread))")
}
printContext()
Task { @MainActor in
printContext()
}
Task.detached { @MainActor in
printContext()
}
Task {
await MainActor.run {
printContext()
}
}
DispatchQueue.main.async {
printContext()
}
dispatchMain()
According to the global actor proposal, I would expect DispatchQueue.main.async { ...
to be roughly equivalent to Task.detached { @MainActor in ...
.
Yet with Swift 5.6.1 on arm64-apple-macosx12.0
, the program seems to nondeterministically yield different results upon invocation. Sometimes I get the expected output:
At main:7: Running on <_NSMainThread: 0x600000083c80>{number = 1, name = main} (main: true)
At main:10: Running on <_NSMainThread: 0x600000083c80>{number = 1, name = main} (main: true)
At main:19: Running on <_NSMainThread: 0x600000083c80>{number = 1, name = main} (main: true)
At main:14: Running on <_NSMainThread: 0x600000083c80>{number = 1, name = main} (main: true)
At main:24: Running on <_NSMainThread: 0x600000083c80>{number = 1, name = main} (main: true)
Sometimes the @MainActor
closures seem to execute on another thread:
At main:7: Running on <_NSMainThread: 0x600002ae44c0>{number = 1, name = main} (main: true)
At main:24: Running on <_NSMainThread: 0x600002ae44c0>{number = 1, name = main} (main: true)
At main:10: Running on <NSThread: 0x600002afff00>{number = 2, name = (null)} (main: false)
At main:19: Running on <NSThread: 0x600002afff00>{number = 2, name = (null)} (main: false)
At main:14: Running on <NSThread: 0x600002afff00>{number = 2, name = (null)} (main: false)
Only the DispatchQueue
mechanism seems to reliably schedule onto the main
thread. Am I misunderstanding part of the concurrency model or why does the program behave this way?
Upvotes: 8
Views: 3468
Reputation: 438437
Global actors (including the main actor) have had optimizations whereby they could eliminate unnecessary executor hops if the partial task does not do anything that actually requires actor isolation. The net effect is that code will not always run on the thread that you may have expected.
If, however, you change your example such that it explicitly requires actor isolation (e.g., perhaps increment a counter isolated to the main actor), then this behavior will disappear.
Upvotes: 0
Reputation: 11
Yes, async/await abstracts threads away. Under the hood, there's a thread pool and, when you run a Task, you basically say, when there's time available (and given a priority), run this code. Your code may suspend on one thread, and resume on another within the same Task.
Thus, code ran within a Task can be expected to run on random threads. To run code on the main thread, you want to use await MainActor.run
. Otherwise, you have no guarantee.
Upvotes: 0