Making Thread.isMainActor unavailable in concurrency - will make it harder to check understanding

The context inheritance rules for Tasks can be surprising at first glance.
For example - I think it will be a common 'gotcha' for people to send 'background' work off to a Task from setup in a UIViewController - not realising that this task will inherit the class @MainActor context.

I just got stuck into a twitter conversation related to this - and frankly wasn't 100% sure that I was right without running a quick test.

You can see my test above.

My issue is that running this test will become impossible in Swift6 because Thread.isMainThread is becoming available.

I many be missing some other way for people to check that they are reasoning correctly about threads (other than setting a breakpoint and inspecting manually).

If not - then I think one is needed...

Perhaps I’m misunderstanding, but it seems as though in Swift 6 you’ll still be able to verify your understanding statically by attempting to (synchronously) call any @MainActor function and see whether it compiles.

  1. the compiler is currently giving the warning (soon to be error) even in functions which are definitely @MainActor (inherited from parent)
    I may be misunderstanding the error - but it seems to say you can't call from any asynchronous context - even if it is explicitly @MainActor

  1. the compiler's view of what happens - and what actually happens can be very different!

I didn’t mean to suggest that you call Thread.isMainThread from a function that is @MainActor—rather, I was saying that with full concurrency checking enabled you’ll check the “is main thread” condition at compile time by simply attempting to call an @MainActor function without await and if the code compiles then you know whenever that code executes it will be on the main thread.

That makes more sense.
It's still relying on what the compiler thinks - rather than what actually happens though...

The main actor guarantees that its jobs all run on the main thread. I won’t say it’s impossible that something else would ever happen, but if it did it would be a very serious bug. And, of course, if you allow for serious bugs then you have to consider that Thread.isMainThread could somehow have a bug that allowed it to report the wrong result as well. :)

there are loads of ways that @MainActor fails to guarantee work runs on the main thread!

https://blog.hobbyistsoftware.com/2022/11/mainactor-the-rules/

2 Likes

Ah, yes, if you go through Objective-C machinery then you lose some static guarantees. I consider the keypath issue noted there to fall under “serious bug.”

like using Timer, or CoreBluetooth, or NSAlert...

my point is that most devs are using Objective-C machinery most of the time..

1 Like

Would dispatchPrecondition work for you?

        dispatchPrecondition(condition: .onQueue(.main))
        dispatchPrecondition(condition: .notOnQueue(.main))
1 Like

When dispatching via the Task initializers, though, as in the example at the top of the thread, (I believe) all the Swift guarantees should apply.

1 Like

Nice - I wasn't aware of that. Thank you.

Any insight on why dispatchPrecondition is 'allowed' to check the thread while Thread.isMainThread isn't?

No idea, maybe one of these wasn't annotated properly (which one – to be seen :wink:).

I'd also check if you can call a C function - if you can – it's quite a "superpower" (or, a "backdoor" if you want to look at it that way), from there you can call arbitrary pthread or dispatch APIs without restrictions); and as with "unsafe" the onus would be on you to use it responsibly.

If (Objective-)C is the way for a @MainActor function to wind up off the main thread, then (Objective-)C can be the way to assert that it’s on the main thread. :wink:

2 Likes

That's just a warning, right? Just ignore it for now. Even if it claims "this is an error in Swift 6" - by that time either the API will change, or your code will totally change, or you switch to Kotlin, or you quit programming, etc, etc. Follow the YAGNI principle and deal with the actual problem only when (and if) needed :wink:

And if the warning is really bothering you now (e.g. if you want to enable a setting to treat all warnings as errors and that's the only thing blocking you), remember the check is very shallow and can be easily suppressed:

print("on main \(Thread.isMainThread)") // 🔶 Warning: Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in Swift 6

print("on main \(Thread.isMain)") // No warning whatsoever

where:

extension Thread {
    static var isMain: Bool { isMainThread }
}
1 Like