The call to the launch() and runBlocking() functions resulted in the coroutines executing in the same thread as the caller’s coroutine scope. That’s the default behavior of these function since they carry a coroutine context from their scope. You may, however, vary the context and the thread of execution of the coroutines where you like.

Explicitly setting a context

You may pass a CoroutineContext to the launch() and runBlocking() functions to set the execution context of the coroutines these functions start.

The value of Dispatchers.Default for the argument of type CoroutineContext instructs the coroutine that is started to execute in a thread from a DefaultDispatcher pool. The number of threads in this pool is either 2 or equal to the number of cores on the system, whichever is higher. This pool is intended to run computationally intensive tasks.

The value of Dispatchers.IO can be used to execute coroutines in a pool that is dedicated to running IO intensive tasks. That pool may grow in size if threads are blocked on IO and more tasks are created.

Dispatchers.Main can be used on Android devices and Swing UI, for example, to run tasks that update the UI from only the main thread.

To get a feel for how to set the context for launch(), let’s take the previous example and make a change to one of the launch() calls, like so:

runBlocking {
  launch(Dispatchers.Default) { task1() } 
  launch { task2() }
  println("called task1 and task2 from ${Thread.currentThread()}") 

After this change, the code in task1() will run in a different thread than the rest of the code that still runs in the main thread. We can verify this in the output—the output you see may be slightly different since the order of multiple threads running in parallel is nondeterministic:

start task1 in Thread Thread[DefaultDispatcher-worker-1,5,main] end task1 in Thread Thread[DefaultDispatcher-worker-2,5,main] called task1 and task2 from Thread[main,5,main]
start task2 in Thread Thread[main,5,main]
end task2 in Thread Thread[main,5,main]

In this case, the code within the lambda passes to runBlocking(), and the code within task2() runs concurrently, but the code within task1() is running in parallel. Coroutines may execute concurrently or in parallel, depending on their context.

Running in a custom pool

You know how to set a context explicitly, but the context we used in the previous example was the built-in DefaultDispatcher. If you’d like to run your coroutines in your own single thread pool, you can do that as well. Since you’ll have a single thread in the pool, the coroutines using this context will run concurrently instead of in parallel. This is a good option if you’re concerned about resource contention among the tasks executing as coroutines.

To set a single thread pool context, we first have to create a single thread executor. For this we can use the JDK Executors concurrency API from the java.util.concurrent package. Once we create an executor, using the JDK library, we can use Kotlin’s extension functions to get a CoroutineContext from it using an asCoroutineDispatcher() function. Let’s give that a shot.

First, import the necessary package:

// single.kts
import kotlinx.coroutines.*
import java.util.concurrent.Executors
//...task1 and task2 function definitions as before...

You may be tempted to create a dispatcher from the single thread executor and pass that directly to launch(), but there’s a catch. If we don’t close the executor, our program may never terminate. That’s because there’s an active thread in the executor’s pool, in addition to main, and that will keep the JVM alive. We need to keep an eye on when all the coroutines complete and then close the executor. But that code can become hard to write and error prone. Thankfully, there’s a nice use() function that will take care of those steps for us. The use() function is akin to the try-with-resources feature in Java. The code to use the context can then go into the lambda passed to the use() function, like so:

Get hands-on with 1200+ tech skills courses.