@MainActor closure does not seem to execute on the main thread deterministically

Viewed 67

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?

1 Answers

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.

Related