GlobalScope in Kotlin Coroutines: Is It Really Worth the Risk?

Written by jesperancinha | Published 2024/03/17
Tech Story Tags: kotlin | coroutines | global-scope | kotlin-coroutines | globalscope-in-kotlin | coroutine-lifecycle | android-development | coroutine-management

TLDRThe `GlobalScope` is a kind of scope in the world of `Kotlin` coroutines that challenges a lot of developers. Many say that it is not useful and it was probably a bad idea to introduce it in the standard library any ways. The reason for this seems to stem of of the fact that we cannot tie the `Global Scope` created cor outines with any specific lifecycle. In the video, to which you can find the link bellow, I am casually talking about that while walking in Gouda.via the TL;DR App

The GlobalScope is a kind of scope in the world of Kotlin coroutines that challenges a lot of developers and prompts many to say that it is not useful, and it was probably a bad idea to introduce it in the kotlix standard library anyway since it’s not highly recommended. The reason for this seems to stem from the fact that we cannot tie the GlobalScope created coroutines with any specific lifecycle. This can be problematic in any kind of programming, but especially android programming where we mostly tie coroutines to the lifecycle of fragments or activities. In the video, to which you can find the link below, I am casually talking about that while walking in Gouda by the Groenhovenpark.


To exemplify how the GlobalScope operates and why it can be a problem to use it I created an example that you can explore by issuing these commands:

git clone https://github.com/jesperancinha/jeorg-kotlin-test-drives.git
cd jeorg-kotlin-coroutines/coroutines-crums-group-1
make b

The example class is GlobalScopeCoroutine which is an executable class and the code is this one:

class GlobalScopeCoroutine {
    companion object {
        @OptIn(DelicateCoroutinesApi::class)
        @JvmStatic
        fun main(args: Array<String> = emptyArray()) {
            val job = GlobalScope.launch {
                delay(1.seconds.toJavaDuration())
            }
            println("Global >> Is the Global job cancelled? ${job.isCancelled}")
            println("Global >> Is the Global job active? ${job.isActive}")
            println("Global >> Is the Global job completed? ${job.isCompleted}")
            job.cancel()
            println("Global >> Is the Global job cancelled after cancel? ${job.isCancelled}")
            runBlocking {
                val jobInScope = launch {
                    delay(1.seconds.toJavaDuration())

                }
                jobInScope.cancel()
                println("First Job >> Is this job cancelled? ${jobInScope.isCancelled}")
            }
            val lastJob = runBlocking {
                val jobInScope = launch {
                    delay(1.seconds.toJavaDuration())

                }
                println("Second Job >> Is this job cancelled? ${jobInScope.isCancelled}")
                println("Second Job >> Is this job active? ${jobInScope.isActive}")
                println("Second Job >> Is this job completed? ${jobInScope.isCompleted}")
                jobInScope
            }
            println("Second Job After Life-Cycle >> Is this job cancelled? ${lastJob.isCancelled}")
            println("Second Job After Life-Cycle >> Is this job active? ${lastJob.isActive}")
            println("Second Job After Life-Cycle >> Is this job completed? ${lastJob.isCompleted}")
        }
    }
}

In the first part of the code we can launch a coroutine wih the GlobalScope. Immediately we should observe that we can launch this coroutine without the need to already be in a Global scope. In this case, we only have access to our job in our localstack. This is the only place where we have access to a provision to be able to cancel that coroutine. We can also observe that nothing else is bound to that coroutine and that is is now running without any attachment to any lifecycle. After this call in a real program, this coroutine would be running in the background until it would stop or it would remain running in case of a problem without us being able to stop it anywhere in the application. We can of course still cancel it in this localstack.


In the following runBlocking coroutine builder, we launch a coroutine in scope and we can observe that we can also cancel it in the same way as we did before with the GlobalScope.

It is in the last runBlocking call that we can observe something different. In this case, we do not cancel the coroutine. Instead, we just let it run until the lifecycle ends. We can finally observe that this coroutine is not completed in the blocking scope but when it comes out, it does get completed:

Global >> Is the Global job cancelled? false
Global >> Is the Global job active? true
Global >> Is the Global job completed? false
Global >> Is the Global job cancelled after cancel? true
First Job >> Is this job cancelled? true
Second Job >> Is this job cancelled? false
Second Job >> Is this job active? true
Second Job >> Is this job completed? false
Second Job After Life-Cycle >> Is this job cancelled? false
Second Job After Life-Cycle >> Is this job active? false
Second Job After Life-Cycle >> Is this job completed? true

It is always quite difficult to explain this phenomenon, but the whole idea is that, if we have our framework manage the lifecycle of the coroutines we use then that is much better than using a detached scope like the GlobalScope.


In the literature, we find that GlobalScope may still be used for some exceptional purposes like logging and monitoring. However for that purpose, there are already several frameworks that allow us to do so and this is the reason why for the most part, GlobalScope is always 100% not advised to use.


https://youtube.com/shorts/wqL_1imGhaY?feature=shared&embedable=true

Also published here.


Written by jesperancinha | Software Engineer for 10+ Years, OCP11, Spring Professional 2020 and a Kong Champion
Published by HackerNoon on 2024/03/17