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
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¹:
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
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.
launch(Dispatchers.Default) creates children coroutines in
runBlocking scope, so
runBlocking waits for their completion automatically.
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
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:
Voilà! It completes in a second and it does not depend on implementation details of
work anymore, as long as
work does not block the caller.
¹ ^ If we flip the order of
measureTimeMillis calls in our examples, then we measure nothing, since
launch, by itself, completes quickly and does not wait for the job it launched to complete. But
runBlocking does wait, so we can measure the time it takes.