Structured concurrency in Kotlin Coroutines requires developers to always launch coroutines in the context of CoroutineScope or to specify a scope explicitly. It seems that using GlobalScope is a good default for launching work in background, however we do not recommend using GlobalScope . Why? Let us see it with an example.

Suppose that we have some CPU-consuming or IO-bound blocking task which takes one second. We mock it here using Thread.sleep :

Now we’d like to launch a couple of those tasks concurrently and measure how much time it takes to complete them. The first attempt at doing so looks like this¹:

It prints Work 1 done and Work 2 done , but it takes two seconds to complete. Where’s concurrency? There is none — here, launch had inherited coroutine dispatcher from the scope introduced by runBlocking coroutine builder, which confines execution to the single thread, so both tasks execute sequentially in the main thread.

To get concurrent execution in background threads and complete our work in a second we can launch coroutines with Dispatchers.Default :

Ok. That works and completes in a second.

So, what happens if we use GlobalScope to launch our coroutines? It should be the same, since it executes coroutines in background threads using Dispatchers.Default , too, should not it?

It completes without ever printing Work XXX done once! How come? Let us take a closer look at the difference between these two ways to launch a coroutine.

The launch(Dispatchers.Default) creates children coroutines in runBlocking scope, so runBlocking waits for their completion automatically.

However, GlobalScope.launch creates global coroutines. It is now developer’s responsibility to keep track of their lifetime. We can “fix” an approach with GlobalScope by manually keeping track of the launched coroutines and waiting for their completion using join :

Now this example with GlobalScope works similarly to the code with launch(Dispatchers.Default) , but takes quite more effort, so why bother writing more code? There is hardly ever reason to use GlobalScope in an application that is based on Kotlin coroutines.

Developers used to go to great lengths to keep track of concurrent and asynchronous tasks they launch to make sure they do not leak and to be able to cancel them. With structured concurrency of Kotlin coroutines it is no longer needed. Write the simplest code that works, and it does the right thing by design.

In a larger code-base, though, you should not even use launch(Dispatchers.Default) , but follow the advise outlined in “Blocking threads, suspending coroutines” story. The work function here blocks a thread for a second, but it can be converted into a suspending function using withContext , encapsulating an appropriate dispatcher for its execution:

Now, this suspending version of work does not block its caller, so we can use it from inside a regular launch call and get the concurrency we sought to achieve: