Why not use GlobalScope.launch?
Asked Answered
H

4

55

I read that usage of Globalscope is highly discouraged, here.

I have a simple use-case. For every kafka message (let's say a list of Ids) that I receive I have to split it and invoke a rest service simultaneously for each of those Ids and wait for it to be done and proceed with other synchronous tasks. There is nothing else in that application that requires coroutine. In this case, Can I just get away with using Globalscope ?

Note: This is not an android application. It's a kafka stream processor running on server side. It's an ephemeral, stateless, containerized (Docker) application running in Kubernetes (Buzzword-compliant if you will)

Habilitate answered 23/1, 2019 at 20:45 Comment(0)
J
34

You should scope your concurrency appropriately using structured concurrency. Your coroutines can leak if you don't do this. In your case, scoping them to the processing of a single message seems appropriate.

Here's an example:

/* Let's pretend this function gets called when you 
 * receive a new message.
 */
suspend fun onMessage(msg: Message) {
    val ids: List<Int> = msg.getIds()    

    val jobs = ids.map { id ->
        GlobalScope.launch { restService.post(id) }
    }

    jobs.joinAll()
}

If one of the calls to restService.post(id) fails with an exception, the example will immediately rethrow the exception, and all the jobs that hasn't completed yet will leak. They will continue to execute (potentially indefinitely), and if they fail, you won't know about it.

To solve this, you need to scope your coroutines. Here's the same example without the leak:

suspend fun onMessage(msg: Message) = coroutineScope {
    val ids: List<Int> = msg.getIds()    

    ids.forEach { id ->
        // launch is called on "this", which is the coroutineScope.
        launch { restService.post(id) }
    }
}

In this case, if one of the calls to restService.post(id) fails, then all other non-completed coroutines inside the coroutine scope will get cancelled. When you leave the scope, you can be sure that you haven't leaked any coroutines.

Also, because coroutineScope will wait until all child-coroutines are done, you can drop the jobs.joinAll() call.

Side note: A convention when writing a function that start some coroutines, is to let the caller decide the coroutine scope using the receiver parameter. Doing this with the onMessage function could look like this:

fun CoroutineScope.onMessage(msg: Message): List<Job> {
    val ids: List<Int> = msg.getIds()    

    return ids.map { id ->
        // launch is called on "this", which is the coroutineScope.
        launch { restService.post(id) }
    }
}
Jurist answered 24/1, 2019 at 16:57 Comment(9)
@Habilitate Roman Elizarov just posted an article about the same topic: medium.com/@elizarov/…Jurist
what if you try-catch inside GlobalScope.launch { tryIgnore { block() } } Something like this !Tattler
@Killer But then won't know if anything threw an exception. The point of this is to have exceptions bubble up like normal, so that you are able to handle it. Using GlobalScope and ignoring exceptions inside is really not a good pattern.Jurist
True. Will be shifting to Structural Concurrency to replace GlobalLauch.Tattler
Spent whole day trying to wrap my head around why GlobalScope is bad and what every one means my scoped concurrency and this finally hit me: "...They will continue to execute (potentially indefinitely), and if they fail, you won't know about it...". Thanks.Katzen
Thanks for the answer @marstran. I have a similar use case (fire and forget) but for me, the methods inside the launch {...} clause (analogous to restService.post(id)) are still executed one by one. What is needed in order that they run in parallel?Vinyl
Hi @IrinaS., the code inside a single launch-block runs sequentially. If you want to run them in parallell, you need to put them in separate launch-blocks.Jurist
@marstran, thanks for the hint, I did that.. i.e. the launch blocks are executed sequentially.Vinyl
@IrinaS. Ok, then it might be something with the dispatcher you are using. Try using Dispatchers.Default for example.Jurist
B
28

By the docs using async or launch on the instance of GlobalScope is highly discouraged, application code usually should use application-defined CoroutineScope.

If we look at the definition of GlobalScope we will see that it is declared as object:

object GlobalScope : CoroutineScope { ... }

An object represents a single static instance(Singleton). In Kotlin/JVM a static variable comes into existence when a class is loaded by the JVM and dies when the class is unloaded. When you first use of GlobalScope it will be loaded into the memory and stay there until one of the following happens:

  1. the class is unloaded
  2. the JVM shuts down
  3. the process dies

So it will consume some memory while your server application is running. Even if your server app is finished running but process is not destroyed, a launched coroutine may still be running and consume the memory.

Starting a new coroutine from the global scope using GlobalScope.async or GlobalScope.launch will create a top-level "independent" coroutine.

The mechanism providing the structure of the coroutines is called structured concurrency. Let's see what benefits structured concurrency has over global scopes:

  • The scope is generally responsible for child coroutines, and their lifetime is attached to the lifetime of the scope.
  • The scope can automatically cancel child coroutines if something goes wrong or if a user simply changes their mind and decides to revoke the operation.
  • The scope automatically waits for completion of all the child coroutines. Therefore, if the scope corresponds to a coroutine, then the parent coroutine does not complete until all the coroutines launched in its scope are complete.

When using GlobalScope.async there is no structure that binds several coroutines to a smaller scope. The coroutines started from the global scope are all independent; their lifetime is limited only by the lifetime of the whole application. It is possible to store a reference to the coroutine started from the global scope and wait for its completion or cancel it explicitly, but it won't happen automatically as it would with a structured one. If we want to cancel all coroutines in the scope, with structured concurrency, we only need to cancel the parent coroutine and this automatically propagates cancellation to all the child coroutines.

If you don't need to scope a coroutine to a specific lifetime object and you want to launch a top-level independent coroutine which is operating on the whole application lifetime and is not cancelled prematurely and you don't want to use the benefits of the structured concurrency, then go ahead and use global scopes.

Boone answered 23/1, 2019 at 21:44 Comment(2)
Dear downvoter, please elaborate a bit to enlighten meHabilitate
The downvote may be about this answer contradicting the documentation, which specifically states "using async or launch on the instance of GlobalScope is highly discouraged." However, for the cases where you truly want the coroutine lifetime to equal the lifetime of the JVM, the reasoning against GlobalScope is indeed thin.Burseraceous
C
5

In your link it states:

Application code usually should use application-defined CoroutineScope, using async or launch on the instance of GlobalScope is highly discouraged.

My answer addresses this.

Generally speaking GlobalScope may bad idea, because it is not bound to any job. You should use it for the following:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.

Which does not seem to be your usecase.


For more information there is a passage in the official docs at https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency

There is still something to be desired for practical usage of coroutines. When we use GlobalScope.launch we create a top-level coroutine. Even though it is light-weight, it still consumes some memory resources while it runs. If we forget to keep a reference to the newly launched coroutine it still runs. What if the code in the coroutine hangs (for example, we erroneously delay for too long), what if we launched too many coroutines and ran out of memory? Having to manually keep a reference to all the launched coroutines and join them is error-prone.

There is a better solution. We can use structured concurrency in our code. Instead of launching coroutines in the GlobalScope, just like we usually do with threads (threads are always global), we can launch coroutines in the specific scope of the operation we are performing.

In our example, we have main function that is turned into a coroutine using runBlocking coroutine builder. Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine (runBlocking in our example) does not complete until all the coroutines launched in its scope complete. Thus, we can make our example simpler:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch new coroutine in the scope of runBlocking   
        delay(1000L)   
        println("World!")    
    }   
    println("Hello,")  
}

So in essence it is discouraged, because it forces you to keep references and use join, which can be avoided with structured concurrency. (See code example above.) The article covers many of the subtleties.

Campy answered 24/1, 2019 at 16:24 Comment(1)
I wish more documentation highlighted runBlocking - that's exactly what I needed, but as a newbie Android dev building my first app, I hadn't seen this show up anywhere previously.Velvavelvet
U
3

We see a lot of answers about why we shouldn't use global scope.

I just gonna give you a few cases where it should be okay to use GlobalScope

Logging

private fun startGlobalThread() {
    GlobalScope.launch {
        var count = 0
        while (true) {
            try {
                delay(100)
                println("Logging some Data")
            }catch (exception: Exception) {
                println("Global Exception")
            }
        }
    }
}

Saving data in DB This is a special case in our app where we need to store data in DB and then update them to the server in a sequential manner. So when the user pressed save in form we won't wait for the DB to update instead update using GlobalScope.

/**
 * Don't use another coroutine inside GlobalScope
 * DB update may fail while updating
 */
private fun fireAndForgetDBUpdate() {
    GlobalScope.launch {
        val someProcessedData = ...
        db.update(someProcessedData)
    }
}
Upwards answered 1/6, 2021 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.