fwcd
fwcd

Reputation: 121

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

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

Answers (2)

Rob
Rob

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

electricRGB
electricRGB

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

Related Questions