GlobalScope

A CoroutineScope without any Job.

The global scope is used to launch top-level coroutines whose lifecycles are not limited by structured concurrency. Since GlobalScope does not have a Job, it is impossible to cancel all coroutines launched in it. Likewise, there is no way to wait for all coroutines launched in it to finish.

This is a delicate API. GlobalScope is easy to use to create new coroutines, avoiding all bureaucracy of structured concurrency, but it also means losing all its benefits. See the CoroutineScope documentation for a detailed explanation of structured concurrency and a list of ways to obtain a CoroutineScope most suitable for your use case.

Pitfalls

Computations can happen when they have no right to or are no longer needed

Some computations must be scoped to the lifecycle of some entity. For example, after a user leaves a screen in a UI application, it no longer makes sense to obtain the data needed to display that screen, and attempting to update the UI may even crash the application.

GlobalScope.launch {
val record = withContext(Dispatchers.IO) {
fetchLastRecordFromServer()
}
withContext(Dispatchers.Main) {
component.displayLastRecord(record)
}
}

This code is incorrect because it does not take into account the lifecycle of the component: the whole computation should be cancelled if the component is destroyed.

If a coroutine spawned in the GlobalScope actually is tied to a lifecycle of some entity to function correctly, the created coroutines must somehow be stored and then cancelled when the entity is destroyed. The easiest way to do this is to create a CoroutineScope using the constructor function, use it to launch the coroutines, and then cancel it when the entity is destroyed, which will also lead to the coroutines being cancelled.

Resource leaks

A coroutine that never gets cancelled or resumed can be a resource leak.

val requests = Channel<Request>()
GlobalScope.launch {
try {
while (true) {
val request = channel.receive()
socket.send(request)
}
} finally {
socket.close()
}
}

If at some point, everyone stops populating the channel, the coroutine will never resume or cancel, and the socket will not be closed, leading to a resource leak.

Tying the coroutine to some lifecycle will ensure that the coroutine will get cancelled when its work is no longer needed, releasing all resources it holds.

Crashes

GlobalScope does not have a CoroutineExceptionHandler installed. This means that exceptions thrown in coroutines created using launch will lead to platform-specific last-resort error propagation behavior, such as crashing the application (on Android, Kotlin/Native, and JS) or populating the logs with potentially unnecessary information (on non-Android JVM). Please see CoroutineExceptionHandler for details.

GlobalScope.launch {
// depending on your platform, this line can crash your application
throw IllegalStateException()
}

One way to solve this would be to provide a CoroutineExceptionHandler in the launch arguments. However, it is often much simpler to use structured concurrency to ensure propagation of exceptions to the parent coroutine, which usually has a way to report them: for example, coroutineScope or runBlocking will rethrow the exception to the caller.

How to create coroutines if not with a GlobalScope?

GlobalScope is often used by beginners who do not see any other obvious way to create coroutines. This is an antipattern and should be avoided. In this section, a brief overview of the alternatives is provided.

In many typical usage scenarios, it is not necessary to spawn new coroutines at all to run suspend functions. Several coroutines are needed only if the code is expected to run concurrently, and when the operation execute one after another, all suspend functions can be called sequentially:

suspend fun loadConfiguration() {
val config = fetchConfigFromServer() // network request
updateConfiguration(config)
}

This requires that loadConfiguration is a suspend function, which is not always the case. Many coroutine-aware frameworks manage coroutine scopes for you and provide a way to call suspend functions (for example, by requiring you to provide a suspend lambda). Try to see if the function is already called from some other suspend function. If so, you can add the suspend modifier to this function and call it directly.

If a CoroutineScope is necessary after all because several operations need to be run concurrently in the span of a single function call, the coroutineScope function can be used to create a new scope for the function:

// concurrently load configuration and data
suspend fun loadConfigurationAndData() {
coroutineScope {
launch { loadConfiguration() }
launch { loadData() }
}
}

A useful pattern is to use withContext in a top-level suspend fun main() function:

suspend fun main() {
// choose the dispatcher on which the coroutines should run
withContext(Dispatchers.Default) {
// can call `suspend` functions here!
}
}

In top-level code, when launching a concurrent operation from a non-suspending context, an appropriately confined instance of CoroutineScope shall be used instead of GlobalScope.

See docs for the CoroutineScope interface for details on structured concurrency and an extensive list of ways to obtain a CoroutineScope.

GlobalScope vs. Custom CoroutineScope

Do not replace GlobalScope.launch { ... } with a CoroutineScope().launch { ... } constructor function call. The latter suffers from all the pitfalls described above. See the CoroutineScope documentation on the intended usage of the CoroutineScope() constructor function.

Legitimate use cases

There are limited circumstances under which GlobalScope can be legitimately and safely used, such as top-level background processes that must stay active for the whole duration of the application's lifetime. Because of that, any use of GlobalScope requires an explicit opt-in with @OptIn(DelicateCoroutinesApi::class), like this:

// A global coroutine to log statistics every second, must be always active
@OptIn(DelicateCoroutinesApi::class)
val globalScopeReporter = GlobalScope.launch(CoroutineExceptionHandler { _, e ->
logFatalError("Error in the global statistics-logging coroutine: $e")
}) {
while (true) {
delay(1.seconds)
logStatistics()
}
}

Properties

Link copied to clipboard