Hey! I was watching Swift concurrency: Behind the scenes. I learned, that we as developer who use Swift concurrency, shouldn't violate the runtime contract that says not to block threads. It appears that Swift aims to run as many threads as there are CPU cores in the device. However, a point raised at the end of a session caused some confusion. The speaker mentioned that a context switch occurs when we call methods of a
MainActor , as the main thread is separate from the threads in the cooperative pool. This has led to a few questions:
- How many threads are there in the cooperative pool? If the main thread isn't included, does this mean that the actual number of cooperative threads is numberOfCoresInDevice - 1?
- Why isn't the main thread part of the cooperative pool? My assumption is that this could be for optimization purposes, allowing the main thread to be focused on UI tasks; otherwise, any continuation from any thread could be resumed on the main thread after suspension.
- Is the runtime contract violated here: as we do context switch to the main thread, our current thread should be blocked? Or this contract only for us, developers, system can violate it as it wishes? Anyway, looks like in this case we have one thread that is blocked.
- Probably, this question will be answered as a result of the answers to the first three, but anyway: Why couldn't the main thread work like the threads in the cooperative pool? Just receiving continuations that must be executed on the main thread? That would solve the context switching problem.
Context switching does not block anything, fwiw. It can be (relatively) expensive, but that’s not the same as blocking.
I looked up the cooperative pool size, and found this article that suggests it's the same as the core count. In that post he spawns nine threads on an eight-core machine: one for the main thread, and eight for the cooperative pool.
Thank you for the article! In return, I found this article, where the author successfully ran three times more threads than there are cores.
After reviewing these two articles, it appears that as a developer, one should not think in terms of threads.
However, it's quite interesting to see how the work of the main thread is implemented in Swift Concurrency.
The main thread is, as it has for much longer than Swift has existed let alone Swift Concurrency, managed primarily via NSRunLoop. The main dispatch queue is then serviced by the main runloop when run non-reentrantly in the default modes. In Swift Concurrency, the main actor's executor is responsible for dispatching work onto that, just as a regular actor's executor (the default executor) dispatches work onto the cooperative queues as described in the article you linked
Not all processes have a main thread though; it's primarily a UI-related concept, and non-UI processes like daemons don't need it.
Sometimes one wished it would be possible to have an isolation domain per e.g. window instead - the single main thread isolation isn’t that great these days…. But I digress.
You somewhat can, by making more actors and putting them on the main queue using a custom executor.
For those with similar questions: reading the article 'How MainActor Works' (link) helped me gain a better understanding of the basic concepts of
GlobalActor and provided more information to answer my question.
Thanks to everyone for providing information and discussing the questions!
Right, but what we’d be after is parallelism when having unrelated data flows that are presented in separate windows, would be great to not have to serialize the actual SwiftUI / rendering part (which tends to be a bottleneck even with throttling, all preparatory work in background tasks and just pure display code in the main thread… ).