They said to use the Default Dispatchers but I found out it was Unconfined
João Esperancinha
Posted on March 21, 2024
If I would have to point out one of the most confusing Dispatchers in the world of Kotlin coroutines, that would be the Unconfined dispatchers. Called with Dispatchers.Unconfined when creating a coroutine scope, this a kind of dispatcher that doesn’t really care. The whole idea of the Unconfined dispatcher is simply to pick up a Thread for no other goal than to simply use it. There is no other criteria involved when using a Thread.
Coroutines use a system called green threading or also known as user space threading. We can refer to routines as contructs that are designed to gain control of a Thread, but specifically they are managed by the schedulers to run in platform or system threads. By platform threads, we are talking about the threads accessible by the programming language or higher abstraction that we use in our development environment.
When we talk about the unconfined dispatchers, it is important to understand what these terms are. And so let’s get clear to what is green threading and user space threading. These terms are used interchangeably. However, there is a very subtle difference:
User space threading is used to emphasize that it is the user that schedules the threads. In other words, this is what we refer to when we start threads via our code.
Green threading emphasizes on what goes on behind the curtain. Green threads are also called fibers and these are virtual threads that are multiplexed to be used on top of system threads.
This is all something very difficult to visualize. But for the case of the Unconfined Dispatchers this is all we need to know about this part.
Being aware of the existence of platform threads, virtual threads and system threads and how they work is important because the platform threads will then perform this multiplexing mechanism onto specific system threads and this is why in Kotlin, specifically, we can find dispatchers specifically to be used with the CPU, the Dispatchers.Default, dispatchers specific for IO operations called Dispatchers.IO and dispatchers specific for single threaded UI , such as Dispatchers.Main for other operations as alternatives to the Dispatchers.Unconfined context.
Making tests using the Spring Framework we will very easily come across the fact that the Spring Framework uses Dispatchers.Unconfined. When I made this video it was generally said that Dispatchers.Unconfined were to be used in tests only due to their unpredictability:
Dispatchers.Unconfined do not rely on a particular platform scheduling system oriented to a specific kind of Thread, it becomes very unpredictable how coroutines launched like this would perform. There were also mentions of the possibility that Dispatchers.Unconfined could eventually block the execution of the application in production code, but to be honest I have never ever seen that, even working so much with the Spring Framework for server-side programming as I currently do with Kotlin. And to top that up they mention precisely that, because of that risk, all nested coroutines form an event-loop. They explain that in their documentation here:
But hold on. Why is the Spring Framework using Dispatchers.Unconfined in their implementation instead of other Dispatchers like for example the Dispatchers.IO? And why not test this before anything? Well, first let’s have a look at an issue that was opened on GitHub about this:
Can someone elaborate me why this is using Dispatchers.Unconfined instead of Dispatchers.Default? AFAIK, Default should be picked by Default (obvious!).
I am using the below configurations if that would play some role in debugging this,
OpenJDK Runtime Environment JBR-17.0.7+7-985.2-nomod (build 17.0.7+7-b985.2)
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
id("org.springframework.boot") version "3.1.5"
id("io.spring.dependency-management") version "1.1.3"
springCloudVersion="2022.0.4"
All dependencies are managed by spring-cloud-dependencies - 2022.0.4.
Someone had found out that, for whatever reason, the Dispatchers.Unconfined was being used and so I created a service on my GitHub Repo here to prove that:
sdk install java 11.0.9.hs-adpt
sdk use java 11.0.9.hs-adpt
Sequence Diagram
sequenceDiagram
participant USER
participant SPRING
participant SPRING CORE Docs
participant SPRING Details
participant SPRING Packaging
participant Spring Professional
rect rgb(1,130,25)
USER->>SPRING: User studies spring
SPRING->>SPRING CORE Docs: User dives into Spring Core Docs
SPRING CORE Docs->>SPRING Details: User thinks about using all annotation params
SPRING Details->>SPRING Packaging: User
This method will print out a series of useful information to the console about the corroutine that has been called. The suspend function marked with the suspend modifier isn’t itself a coroutine, but it can only be called by coroutines in Kotlin. And that’s what happens when we call the method. This way we get access to the coroutine context and checkout what dispatcher is being used. Important to note is that using the currentCoroutineContext() function, we are accessing the coroutine context via a top level function in the kotlinx coroutines library. Using coroutineContext, we are accessing the getter of a value located in the kotlin standard library itself. They don’t share exactly the same implementation.
In the root of the car-parts-reactive module we’ll find file generated-requests.http and if you are using IntellJ, you can easily make requests to the application with the code contents:
###
GET http://localhost:8080/break
###
GET http://localhost:8080/keys
###
GET http://localhost:8080/parts
###
GET http://localhost:8080/parts/list
###
GET http://localhost:8080/parts/suspend
###
GET http://localhost:8080/parts/correct
When you have the application running, try to run a simple REST request against endpoint :
There should be a result in the console similar to this:
Current Coroutine Context -> [Context2{micrometer.observation=io.micrometer.observation.NoopObservation@52c86bb3, reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda$1234/0x000000080165d210@7166cb50}, MonoCoroutine{Active}@5474a700, Dispatchers.Unconfined]
Current Coroutine Job -> MonoCoroutine{Active}@5474a700
Main Coroutine Context -> [Context2{micrometer.observation=io.micrometer.observation.NoopObservation@52c86bb3, reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda$1234/0x000000080165d210@7166cb50}, MonoCoroutine{Active}@5474a700, Dispatchers.Unconfined]
Main Coroutine Job -> MonoCoroutine{Active}@5474a700
Current Thread -> Thread[#50,reactor-http-epoll-2,5,main]
We can see in all counts that the dispatcher being used is the Dispatchers.Unconfined. If we go back to the issue that was created on GitHub, we’ll see that the answer to why the spring framework uses Dispatchers.Unconfined turns out to be the best solution for their cases, according to them. So what they mean, is that in spite of these dispatchers appear to be limited and restrictive, they apparently, and after testing are actually a very good fit for a reactive framework.
Of course this is great to read because it makes it clear why actually, unconfined dispatchers are used in Spring.
But let’s go back to the basics. I made a few quizzes about unconfined dispatchers and what I like about it is that they are still quite mysterious to me. And let’s get one thing clear before we move on. There can be some confusion regarding context and scope. Scope refers to where the coroutine is available and context to how it gets dispatched. There can be confusion about this because both terms are quite intimately connected to each other. For example, we cannot create a scope without a dispatcher and they go hand in hand. A scope allows me to launch coroutines with keywords like async and/or launch, but a context is related to how that coroutine gets assigned a Thread.
Let’s make two quizzes. We start with this one in this video:
Unconfined can come across as quite strange. In the follow-up to this video, I’ll give the answer to this question and explain in a Nutshell what Unconfined ...
youtube.com
In this video I show a simple implementation of some code that makes some calls and these different calls will print the words “master”, “mouse” and “cat” in different orders. The code is available here:
git clone https://github.com/jesperancinha/jeorg-kotlin-test-drives.git
cd jeorg-kotlin-coroutines/coroutines-crums-group-1
make b
And the code is this one:
privatesuspendfunrunUnconfinedCoroutinesTest(){logger.infoTextSeparator("Unconfined means that all coroutines will execute concurrently on the starting thread in the scope of the parent thread")valjob=CoroutineScope(Dispatchers.Unconfined).launch{launch{logger.infoThread("Cat Before thread ==> ${Thread.currentThread()}")delay(100)logger.info("This is cat @ ${LocalDateTime.now()}")logger.info("Cat context $coroutineContext")logger.infoThread("Cat After Resume thread ==> ${Thread.currentThread()}")}launch{logger.info("This is mouse @ ${LocalDateTime.now()}")logger.info("Mouse context $coroutineContext")}logger.info("This is master @ ${LocalDateTime.now()}")logger.info("Master context $coroutineContext")logger.infoThread(Thread.currentThread())}job.join()}
When we run this code, many things can happen, but one thing we can predict is how the logs are going to work out in the console. One thing about unconfined dispatchers is that they use the same context, same dispatchers, on the first sub coroutines. If these coroutines need to suspend, they may or may not return to the original Thread they were sharing. In the code above they will most likely not return to the original thread. Nesting in coroutines follows some very important rules as mentioned in their documentation:
So this is the same as saying that the initial call executes in the same continuation in the same call-frame. This also means that the first 3 launches will run concurrently. The launch that logs a cat does this too, but beacause it has a delay, it will resume either in the same Thread or another thread. So this means that Mouse and Master will be the first things to see in the log and Cat will be the last log in the console. We can see all of this in this log sample:
This is master @ 2024-03-21T18:49:00.661284475
Master context [StandaloneCoroutine{Active}@6ce139a4, Dispatchers.Unconfined]
Thread[#1,main,5,main]
Master After Resume thread ==> Thread[#1,main,5,main]
Cat Before thread ==> Thread[#1,main,5,main]
This is mouse @ 2024-03-21T18:49:00.703936722
Mouse context [StandaloneCoroutine{Active}@433d61fb, Dispatchers.Unconfined]
Mouse After Resume thread ==> Thread[#1,main,5,main]
This is cat @ 2024-03-21T18:49:00.801217959
Cat context [StandaloneCoroutine{Active}@52c68062, Dispatchers.Unconfined]
Cat After Resume thread ==> Thread[#28,kotlinx.coroutines.DefaultExecutor,5,main]
And this explains the answer I give to this quiz on this video:
You can find the project that I've made exploring the different scopes and a lot more about Kotlin Coroutines here:- https://github.com/jesperancinha/jeorg-k...
youtube.com
But moving on. I have another quiz for you and it is this one on this video:
Another quiz about #unconfined #coroutines in #kotlin! Have fun! I really need to improve my handwriting! Hope you enjoy it! Visit my repo at https://github....
youtube.com
The code for this quiz is available here:
git clone https://github.com/jesperancinha/jeorg-kotlin-test-drives.git
cd jeorg-kotlin-coroutines/jeorg-kotlin-coroutines-unconfined
make b
And this is the class we are discussing:
objectUnconfinedCoroutineLauncher{privatevallogger=object{
fun info(logText:Any?)=ConsolerizerComposer.out().yellow(logText)funinfoBefore(logText:Any?)=ConsolerizerComposer.out().green(logText)funinfoAfter(logText:Any?)=ConsolerizerComposer.out().red(logText)funinfoTitle(logText:String)=ConsolerizerComposer.outSpace().cyan(ConsolerizerComposer.title(logText))}@JvmStaticfunmain(args:Array<String>=emptyArray())=runBlocking{logger.infoTitle("Unconfined Coroutines scope tests / There are currently ${
Runtime.getRuntime().availableProcessors()}CPU'savailable"
)CoroutineScope(Dispatchers.Unconfined).launch{launch{logger.info("Running on context $coroutineContext")logger.infoBefore("Siamese Cat is launching on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")delay(1)logger.infoAfter("Siamese Cat just ran on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")}launch{logger.info("Running on context $coroutineContext")logger.infoBefore("Mouse is launching on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")delay(20)logger.infoAfter("Mouse just ran on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")}launch{logger.info("Running on context $coroutineContext")logger.infoBefore("Maine Coon is launching on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")delay(30)logger.infoAfter("Maine Coon is just ran on Thread-${Thread.currentThread().name} with id ${Thread.currentThread().threadId()}")}}.join()}}
The best way to pick one or more options form the one I give in the quiz, is to look at the order the animals start. In this case the can all start in random orders. However they will all share the same thread because they are just being launched from that call stack. This way, the Siamese Cat, the Mouse and the Main Coon need to share he same Thread number on those calls and so that eliminates already options B and D. The Siamese cat only has a delay of 1 ms and that means that, although seemingly unlikely, option A is still very possible. It is actually very likely that this coroutine starts and ends before any of the other two start. 1 ms is just too much of a short time. We have to consider that possibility as well. And this is why the right answers to this questions are A and C, and this is only one example of that result:
Running on context [StandaloneCoroutine{Active}@4de5031f, Dispatchers.Unconfined]
Siamese Cat is launching on Thread-main with id 1
Siamese Cat just ran on Thread-main with id 1
Running on context [StandaloneCoroutine{Active}@691a7f8f, Dispatchers.Unconfined]
Mouse is launching on Thread-main with id 1
Running on context [StandaloneCoroutine{Active}@50a7bc6e, Dispatchers.Unconfined]
Maine Coon is launching on Thread-main with id 1
Mouse just ran on Thread-kotlinx.coroutines.DefaultExecutor with id 21
Maine Coon is just ran on Thread-kotlinx.coroutines.DefaultExecutor with id 21
Find the answer to this quiz here:
My idea with this article is to show some interesting cases I found with Dispatchers.Unconfined. You may find these issues while working with Kotlin coroutines and very likely a lot of people will tell you just to use the Default dispatcher. That can lead to a miscommunication problem. There is such a thing called Dispatchers.Default , which is specific for CPU operations. When people say that when in doubt, use the default dispatcher, they probably do not mean the Dispatchers.Default, context. Make sure to double check, because, especially if you are working in server-side programming with certain frameworks, what they probably mean is that you shouldn’t use any specific dispatchers. Most of frameworks like the Spring Framework already offer support to use suspend functions and so your concern as a developer is for the most cases only to implement the suspend functions and not use any specific context. Use the default of course and that, in Spring is Dispatchers.Unconfined. Have good one everyone!