6

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?

fwcd
  • 81
  • 4
  • remove dispatchMain() – cora Aug 31 '22 at 21:03
  • 1
    Without `dispatchMain` the program exits immediately without executing the asynchronous blocks, so we need some form of blocking the main thread to see the effects of this. – fwcd Sep 01 '22 at 11:21
  • Runs on main thread consistently in my sample app after removing it. – cora Sep 01 '22 at 12:32
  • FWIW, when I run in a command line tool, I see the behavior you describe, but when I run in an app, I have not yet reproduced it. That having been said, I definitely have encountered situations where code was not running on the thread I expected, but then again, when I insert code that requires actor isolation, for example, it always runs on the right thread. – Rob Sep 01 '22 at 16:03
  • Part of how `async`-`await` achieves performance improvements is to avoid costly context switches from one thread to another, wherever it can. The WWDC 2021 video, [Swift concurrency: Behind the scenes](https://developer.apple.com/videos/play/wwdc2021/10254/), will not directly answer your question, but might provide some insights how Swift concurrency threading patterns differ from traditional GCD patterns. – Rob Sep 01 '22 at 16:03
  • From what I understand of Swift concurrency (which I'm definitely not very experienced in) is that the _callee_ defines the task/thread, not the _caller_. That said, I don't know how to force some code to run on the main thread - I just know that if you want a method for example to always run on the main actor, you give it the `@MainActor` attribute. In this case you do make the method for yourself so you can easily add this attribute. – George Sep 05 '22 at 15:28

1 Answers1

-1

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.

  • Sure, but I would expect `@MainActor` to force the runtime to schedule onto the main thread. As noted in my example, even `MainActor.run` doesn't seem to address this reliably. – fwcd Sep 01 '22 at 11:40
  • Actor isolation guarantees that only one code block is executing in the actor's isolation context at any given moment. So long as only one thread at a time (whatever thread that happens to be) is running `@MainActor` isolated code , the actor isolation is protected. The computer can run `@MainActor` isolated code on any thread so long as that is the ONLY thread running isolated code. To put it another way, at any given moment the computer can choose which thread is running `@MainActor` isolated code and for that interval the thread *is* the "main thread" (even if it's not _NSMainThread) – Scott Thompson Sep 10 '22 at 00:09
  • @ScottThompson How do you know all that? – Midnight Guest Feb 06 '23 at 22:39
  • @MidnightGuest it's the definition of what an `actor` is. In The Swift Programming guide actors "... allow only one task to access their mutable state at a time, which makes it safe for code *in multiple tasks* to interact with the same instance of an actor." (emphasis mine) Each of those "multiple tasks" could be running in individual threads. The actor guarantees that they have serialized access to the mutable state of the main execution context but does not require them to execute code on the main thread. – Scott Thompson Feb 06 '23 at 23:58
  • The main actor is special. It always runs code on the main thread. (I believe nothing else will ever do this, to boot; the concurrent pool is entirely isolated from the main thread.) – saagarjha Jun 22 '23 at 00:10