GlobalScope and runBlocking should not be used, so how do you start a coroutine?

All of the coroutine videos and blog posts are awful and leave out important details. GlobalScope and runBlocking should not be used, from the docs, so how do you start a coroutine? Launch and Async need an existing coroutine, so it gets very confusing when almost all examples out there use runBlocking from main or other, GlobalScope, or suspend fun main.

Say I make a suspend function, oh another thing that was said to be avoided! Maybe I am using a library that has a suspend function.

This suspend function I want to call from fun main without using runBlocking or GlobalScope, is there any example out there in tutorial form or video that actually shows this?

I think I need a scope with a job and then I can launch, why do they skip such important details.

Not using suspend fun main either, how would you kick off the coroutines somewhere in your application from Kotlin code.

7 Likes

Wow, I looked at the official coroutine guide and you are right, it doesn’t go into much detail about how to start coroutines in a real-world situation.

launch and async are extension methods on CoroutineScope. So you need to create a scope with either CoroutineScope() (which uses the default dispatcher) or MainScope() (which uses the main dispatcher). If you are programming in Android then activities, fragments, and view models will create their own scopes you can use.

You create the scope at the beginning of your coroutines’ lifecycle, and you cancel() it when the lifecycle is over.

3 Likes

From my experience, people in the internet are overreacting with this "Do not use GlobalScoper/runBlocking()!`. Then they often provide alternatives that are functionally the same to above, so it seems they don’t really understand why these components are discouraged.

Both of them have their uses, they’re not evil or something. But it is important to understand their drawbacks, because they are easily overused.

  • runBlocking() - use when you need to bridge regular and suspending code in synchronous way, by blocking the thread. Just don’t overuse it. Don’t treat runBlocking() as a quick fix for calling suspend functions. Always think if instead you shouldn’t propagate suspending to the caller. Or use futures/callbacks. Or separate regular and suspending worlds and leave just few connections between them. If you start adding runBlocking() in all places in your code then this is really a bad use of it.

  • GlobalScope - use if you don’t care about structured concurrency. If you need to launch some coroutine that you won’t ever cancel, you won’t check its status, you don’t care about its failures, etc. then GlobalScope is just fine (actually, even when using GlobalScope we can still store Job and handle failures). However, you should take a moment to think if you really don’t care about all these things. It’s annoying and sometimes hard to debug if one asynchronous task fails and then another goes crazy.

  • Implement some kind of a service with clearly defined lifecycle and create CoroutineScope for it. This is described here. It bridges traditional way of running asynchronous services in reliable manner with structured concurrency of coroutines.

Generally speaking, this is more complicated than in other concurrency tools/frameworks (including threads) only because coroutines try to fix the problem of cancelling and failures of asynchronous tasks. Usually, it is discouraged to “just start a coroutine”, because then we don’t control its state. We are encouraged to explicitly choose what should happen in exceptional states.

edit:
Re: GlobalScope - also, even if we don’t care about structured concurrency, then it still makes sense to create our own scope. This is very cheap, just a one line of code and we get additional advantages: we can configure all coroutines in one place, it will be easier to add structured concurrency in the future, etc.

9 Likes

runBlocking should not be used from inside the coroutine, but it is a normal way to enter the coroutine world.

GlobalScope could be used, but probably not more than once per program. It also allows you to enter the coroutine world. The trick is not to jump to and from coroutine world. If you enter it once, then live within it.

Anothe way is to use suspend main funciton. It works more or less the same way as wrapping the whole program in GlobalScope.launch.

7 Likes

Thanks for your reply. I am not programming in Android and that is one of the issues I hit right away, because no scopes were there to use. I did get past that though and see the need for a scope. The examples though should be better about indicating this.

Thanks for your reply. The suspend main was a useful thing to learn. It is just so unfortunate that examples just don’t really convey it well for examples.

Thanks for the detailed explanation and it is appreciated. It did help. A point worth making is if you are working with existing code the integration story is weak. Most examples assume a very very simple main or application.

Also, what if you are writing a library and you want a good api story. runBlocking and GlobalScope should be better understood I would think. I don’t know what the answer is, but the docs, don’t get it right at least for me.

1 Like

I found this blog post from 2018 somewhat helpful

1 Like

Thanks it does have just about the best explanation up to a point.

It is good, which the best part is “Bridging the normal world and the suspending world”, but it is the smallest and weakest. It ends poorly with using runBlocking from main. Sure, nothing wrong with that in itself, but it assumes you have control over main and then all you code is running from it.

So, anyone else who comes across it should only read 2/3 of it. The last examples end up right were this question begins, use of GlobalScope and runBlocking. It really should have demonstrated scope creation and existing code integration, something more typical of what really will happen.

I understand the Kotlin team has went so far as, ended up rolling back, some runBlocking irritant. And GlobalScope is “delicate”. So, examples should be teaching proper ways, not demo code. Just my observations.

3 Likes

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 :slight_smile:

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 Flows. 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.

5 Likes

Thanks for the reply and explanation.

Thanks all, but anyone reading this should know, I am out, had enough hassle, goodbye Kotlin.