Thanks for the feedback. I wrote this article quite some time ago, and maybe it needs a bit of refreshing indeed. The aim of the article was to understand the mechanics of coroutines, not to explain the best practices for it. Maybe this calls for a second article
I haven’t re-read the coroutines doc since GlobalScope
has clearly been marked as delicate, but I think they have removed its usages in the examples.
The problem with giving better examples of real-life scope usages is that it depends on the framework you’re using. This is because the proper usage of coroutine scopes is when you create them in the context of something with a lifecycle, and cancel it appropriately when this thing dies or is not used anymore.
This might feel a bit abstract, but essentially the goal is to simplify the rest of the code. If you start all coroutines as children of bigger scopes, you defer the responsibility of “cancelling at the right time” to the component that defines the parent scope. But, you still have that responsibility when defining the parent scope.
- In applications where you control the
main()
method, runBlocking
is usually the way to go, because that’s your entrypoint (you may use suspend fun main()
as well, but then you’ll need to define a scope anyway with coroutineScope { ... }
and the likes if you want to start coroutines). It’s definitely not a bad thing to use runBlocking
in this context.
- In Android, you have predefined scopes based on the lifecycle of the Android components (especially UI stuff)
- In UI-related frameworks that don’t natively integrate with coroutines, there usually is some kind of lifecycle hook for the “destruction” of a component, and in that case you can create a custom scope with a factory function like
CoroutineScope(..)
as a property of your component, and cancel()
that scope in this destructor provided by the framework. This ensures all coroutines started in the context of this component are cancelled when not needed anymore, and prevents leaks (that’s the point of structure concurrency).
- In the entrypoints of non-UI frameworks like web servers, it might be less clear what the lifecycle of a component is and how to define a proper scope. It all depends on how you want to limit the lifetime of your coroutines. If there is no lifecycle, no clear “end” for the coroutines cleanup to occur, then you just accept potential leaks of coroutines (you’re conceptually forced to). You may either use
GlobalScope
or define your own global scope in a singleton or global variable (which you can configure with the dispatcher and exception handler you want, which at least improves a bit on GlobalScope
).
- In Spring, you can do better. You can choose to handle requests asynchronously by using the reactive flavor (WebFlux) which lets you use
suspend
functions directly in controllers (the entrypoints), so you are already in the suspending world. You can then simply use coroutineScope
to start coroutines when you need to do some concurrent work, like you would do in a regular suspend function call hierarchy. Spring will handle the cancellation of individual request handlers automatically because it integrates coroutines cancellation with request timeouts.
- In other kinds of “entrypoint” like integration with callback based systems, there are good use cases for
runBlocking
as well. Although most of the time you can wrap those callback-based approaches into suspend functions or flows.
Note that runBlocking
is not only for main()
. It’s not necessarily a bad thing in the middle of your stack, as long as it’s at the boundary between the code that doesn’t know about coroutines and the code that uses suspend
functions.
Also, as mentioned above, sometimes even a fully migrated application is still called from a non-suspending context by whatever external library/framework callback and runBlocking
is warranted if that library expects the work to be completed when the function returns.
In short, there is simply no other way if the place that calls you is a blocking API that expects the work to be done synchronously. Just use runBlocking
there, it’s meant for this.
If you’re writing a library, I 100% encourage you to strive for exposing suspend functions and Flow
s. Try to avoid choosing scopes for the user, and especially don’t use GlobalScope
in that case.
If you create components with their own event loops, or thread pools, or things like this that manage their own coroutine lifecycle, then you sort of have to create custom scopes. In this case, expose a function to dispose of them like close()
(and make them implement AutoCloseable
if you can, it’s always a plus).
Also if you do this, a good practice would be to accept a coroutine context as parameter from your users so they can customize the behaviour (you can default to EmptyCoroutineContext
). This is also pretty useful for you as a library author when you write coroutine tests with runTest
and you want to inject the test scope.