Class property 'isMainThread' is unavailable from asynchronous contexts; Work intended for the main actor should be marked with @MainActor; this is an error in the Swift 6 language mode
I'm curious, is there a technical reason for this (as in, it would be too difficult to maintain correctness)? Or is it to encourage us to stop thinking about threads?
If it's the latter, may I offer that adding in these runtime checks helps us understand that our mental model of swift concurrency matches the true reality.
At runtime, processes will spit out things like "Publishing changes from background threads is not allowed", yet tools that help us prevent that are being taken away.
I found in a thread from two years ago that dispatchPrecondition(condition: .onQueue(.main)) works without a compiler warning. It's still true today, which I'm happy about. But if correctness can be maintained for the dispatchPrecondition why can't it be maintained for Thread.isMainThread?
I'd also just like to offer one more thing: these swift concurrency warnings encourage folks that otherwise use Treat warnings as errors to flip it off. So we lose some hygiene with these warnings too.
I donât have answers to your actual questions, but I did want to share a titbit: You can avoid warnings like this by wrapping the call. For example, replace this:
func test() async {
print(Thread.isMainThread)
// ^ Class property 'isMainThread' is unavailable from
// asynchronous contexts; Work intended for the main actor should
// be marked with @MainActor
}
Taking a step back, this whole class of APIs is problematic because folks use them for two different reasons:
To assert invariants, for example, to trap if youâre not running in the right context
To alter runtime behaviour, for example, to bounce to the main thread if youâre not on the main thread
The first usage is fine but the second is a path with many pitfalls. Thatâs why Dispatchâs variant is structured in terms of a precondition.
Finally, Konrad wrote:
The main actor is not necessarily the main thread (though most of the time it is).
Indeed. And that âmost of the timeâ makes things hard because folks write code that assumes this correlation and then that code fails in odd circumstances.
I also want to stress that the main queue is not necessarily serviced by the main thread. Iâve included some fun examples below.
Share and Enjoy
Quinn âThe Eskimo!â @ DTS @ Apple
import Foundation
let queue = DispatchQueue(label: "not-main")
func main() {
queue.async {
DispatchQueue.main.sync {
// This doesnât trap:
dispatchPrecondition(condition: .onQueue(.main))
// But this prints false:
print(Thread.isMainThread)
}
}
dispatchMain()
}
main()
import Foundation
let queue = DispatchQueue(label: "not-main")
func main() {
DispatchQueue.main.async {
queue.sync {
// Neither of these trap:
dispatchPrecondition(condition: .onQueue(.main))
dispatchPrecondition(condition: .onQueue(queue))
// This prints true:
print(Thread.isMainThread)
}
}
dispatchMain()
}
main()
Calling dispatchMain() immediately deletes the main thread entirely, so once itâs been called, nothing can ever run on the main thread.
(Note that this means this issue will never apply to app programmers because calling dispatchMain in an app will break everything. This is a daemon programmer thing)
The main actor is not necessarily the main thread (though most of the time it is).
That's good to know! I thought main actor implied main thread. But now I have to ask:
What are the conditions that lead to main actor code executing on a thread other than main?
Are we supposed to still be using DispatchQueue.main.async and friends in our MainActor code when we want to guarantee we're on main (because the Apple framework demands it, for example)?
Edit to add: I guess that Quinn's examples of the main queue not being serviced by the main thread confuse matters further. It's hard to guarantee you're on main! Perhaps the happy path of MainActor being on main thread is happy enough that I shouldn't worry about adding additional wrinkles to satisfy framework demands
There is only one on Darwin platforms (iOS, macOS, etcâŚ): when the main thread no longer exists (which only happens if it has been explicitly exited, such as by calling dispatchMain)
For pthread_self to return anything, a thread would have to exist to call it
There are various things that can directly create threads in processes to respond to events; for example, incoming XPC messages can be delivered that way.
All that said, I have never personally seen a zero thread process. I've wanted to for years though, it seems like a lovely efficiency win.
From asynchronous contexts, consider MainActor.assertIsolated() instead. It is designed for verifying whether you are on the main actor, or not.
In terms of why you should not use any of the Thread API from asynchronous contexts, I presume it is because there are optimizations that can defy oneâs threading expectations. When Swift concurrency first came out, it was quite aggressive in optimizing code, where, for example, if a particular continuation isolated to the main actor did nothing that actually required the main actor, it didnât always run it on the main thread (!). Recent Swift versions have ratcheted back this optimization: I suspect they did this because so many developers were losing their minds over this optimization, as it defied naive thread expectations.
As a final aside, the vast majority of the time, assertIsolated is simply not needed. This âlet me check what thread Iâm onâ is a common GCD problem (as the thread used by a function is typically dictated by the callerâs thread, and defensive programmers would put in dispatchPrecondition tests to make sure). In Swift concurrency, the asynchronous context of a function is dictated by how you define the function, not by the context of the caller. So, the vast majority of the time, if you find yourself in a situation where you feel like âI want to insert a main actor isolation testâ, the right answer is often to just isolate that function to the correct actor and be done with it.
@louzell â The compiler will sometimes determine whether it needs to be on the main thread or not. For example, if we call two asynchronous functions and put a breakpoint before the second call, you will see that you are not on the main thread. You are still in the cooperative thread pool:
In previous versions of Swift concurrency, it was even more confusing: If you put something in between these two calls which did not require any actor isolation (say, a simple print statement), the compiler would successfully determine that main actor isolation was not required for that particular continuation, and would elegantly optimize out the executor hop back to the main thread, like above. But, nowadays, if you put anything in between these two calls, it will reintroduce the executor hop back to the main thread, whether it is really needed or not. They presumably removed this optimization to avoid unnecessarily confusing programmers about the threading logic. But as you can see above, this optimization is still present, if in a more limited manner.
To be honest, when I first stumbled across this behavior, it felt like a Heisenbug, where the act of observing on which thread it was running (e.g., printing Thread.isMainThread) actually changed on which thread it ran. But in retrospect, it was just a clever optimization going on at compile-time.
But, bottom line, the choice to not permit Thread API within Swift concurrency is a very conscious decision. The MainActor.assertIsolated is the preferred way of determining whether you are isolated to the main actor or not.