Bhaskar Singh
05/25/2025, 11:20 AM<http://Dispatchers.IO|Dispatchers.IO>
, I understand that it's optimized for I/O operations. Internally, does this utilize mechanisms like Java NIO or even OS-level calls such as epoll_create1()
? Is it accurate to think of it as posting a task to the OS and letting the OS notify the application when the I/O completes?
3. CPU-bound tasks and Dispatchers.Default:
4. For CPU-bound tasks, we typically use Dispatchers.Default
, which is backed by a shared pool of threads equal to the number of CPU cores (e.g. 8 threads for 8 cores).
◦ Does this mean that only up to 8 CPU-bound coroutine tasks can be executed concurrently? I know more can be submitted, but they would be scheduled based on thread availability.
◦ Are these tasks tied to specific CPU cores? For instance, if Task A starts on Core 1, will it continue on Core 1 for the sake of cache locality (i.e. core affinity), or is that not guaranteed?
◦ Is time-slicing used when there are more tasks than available cores?
Would really appreciate any clarification or pointers. Thanks in advance!rkechols
05/25/2025, 4:22 PMIO
dispatcher and the Default
dispatcher is the size of their threadpools. <http://Dispatchers.IO|Dispatchers.IO>
has no special hidden functionality to actually make it better at I/O operations. Dispatchers.Default
has a threadpool with size equal to the number of CPU cores, and <http://Dispatchers.IO|Dispatchers.IO>
allows for many more additional threads.
To better understand the intent behind these dispatchers, let's step back from Kotlin and Coroutines for a moment. Imagine your machine has 4 CPU cores. If you launch a program with 4 threads or less running in parallel, each thread can do its work using its own CPU core. If you launch a program with 5 threads or more running in parallel, some of the threads will need to share usage of CPU cores; in this scenario the OS is responsible for scheduling how multiple threads can use the same CPU core, and it will typically have the threads take turns using the CPU core. Threads that are sharing CPU cores will take longer to do their work than ones that each have their own CPU core. CPU cores are the bottleneck on overall computational speed. Coming back to Kotlin coroutines, this is why Dispatchers.Default
only makes n
worker threads for n
CPU cores; more worker threads will do nothing to speed up CPU-bound work (and might even slow it down, due to factors such as context-switching, thrashing, etc.)
The whole goal of suspend
functions, coroutines, etc. is to make better use of the CPU cores that you have. If you have one coroutine that at some point needs to wait for something else (perhaps a network call, disk I/O, or the completion of another coroutine) and it's not using any CPU power, the coroutine system allows other tasks to step in and make use of that thread / CPU core that would be otherwise idle.
The trick with <http://Dispatchers.IO|Dispatchers.IO>
is that it's only useful if you have code which you know is waiting on something else (e.g. a network call) but the function you're calling is not declared with `suspend`; this is often the case with low-level system calls. Any non-suspending function holds onto its thread, and doesn't let others use it. This is a blocking
function call; it blocks
the thread from doing other work. If this blocking function is being run on a thread owned by Dispatchers.Default
, this means it's hogging a thread that could be used for other work, and you might have an idle CPU core even though there is work waiting. This is when you want to use <http://Dispatchers.IO|Dispatchers.IO>
, which will run it in an extra thread outside of the n
owned by Dispatchers.Default
. If you know the task is primarily waiting and not using CPU power, this extra thread will not slow down your main CPU work.
Here is a tutorial video that I personally found very helpful in understanding when to use `Dispatchers.IO`:
rkechols
05/25/2025, 4:58 PMBhaskar Singh
05/25/2025, 8:54 PMDon Mitchell
05/27/2025, 12:56 PM<http://Dispatchers.IO|Dispatchers.IO>
replaced the low level IO ops to suspend and thus gave nearly optimal context switching. Alas, it's only at the suspend fun
level