107

I am used to working with AsyncTask and understand it pretty well due to its simplicity. But Coroutines are confusing to me. Can you please explain to me in a simple way what is the difference and purpose of each of the following?

  1. GlobalScope.launch(Dispatchers.IO) {}
  2. GlobalScope.launch{}
  3. CoroutineScope(Dispatchers.IO).launch{}
  4. lifecycleScope.launch(Dispatchers.IO){}
  5. lifecycleScope.launch{}

UPDATE:

After understating all the scopes, I am adding another one with explanation:

viewModelScope.launch(<Dispatcher>){}
viewModelScope.launch{}

This can be used in ViewModel, any coroutine launched in this scope is automatically canceled if the ViewModel is cleared

Dim
  • 4,527
  • 15
  • 80
  • 139

4 Answers4

141

First, let's start with definitions to make it clear. If you need a tutorial or playground for Coroutines and Coroutines Flow you can check out this tutorial/playground i created.

Scope is object you use to launch coroutines that only contains one object which is CoroutineContext

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

The coroutine context is a set of rules and configurations that define how the coroutine will be executed. Under the hood, it’s a kind of map, with a set of possible keys and values.

Coroutine context is immutable, but you can add elements to a context using plus operator, just like you add elements to a set, producing a new context instance

The set of elements that define the behavior of a coroutine are:

  • CoroutineDispatcher — dispatches work to the appropriate thread.
  • Job — controls the lifecycle of the coroutine.
  • CoroutineName — name of the coroutine, useful for debugging.
  • CoroutineExceptionHandler — handles uncaught exceptions

Dispatchers Dispatchers determine which thread pool should be used. Dispatchers class is also CoroutineContext which can be added to CoroutineContext

  • Dispatchers.Default: CPU-intensive work, such as sorting large lists, doing complex calculations and similar. A shared pool of threads on the JVM backs it.

  • Dispatchers.IO: networking or reading and writing from files. In short – any input and output, as the name states

  • Dispatchers.Main: mandatory dispatcher for performing UI-related events in Android's main or UI thread.

For example, showing lists in a RecyclerView, updating Views and so on.

You can check out Android's official documents for more info on dispatchers.

Edit Even though official document states that

Dispatchers.IO - This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.

Answer from Marko Topolnic

IO runs the coroutine on a special, flexible thread pool. It exists only as a workaround when you are forced to use a legacy, blocking IO API that would block its calling thread.

might be right either.

Job A coroutine itself is represented by a Job. A Job is a handle to a coroutine. For every coroutine that you create (by launch or async), it returns a Job instance that uniquely identifies the coroutine and manages its lifecycle. You can also pass a Job to a CoroutineScope to keep a handle on its lifecycle.

It is responsible for coroutine’s lifecycle, cancellation, and parent-child relations. A current job can be retrieved from a current coroutine’s context: A Job can go through a set of states: New, Active, Completing, Completed, Cancelling and Cancelled. while we don’t have access to the states themselves, we can access properties of a Job: isActive, isCancelled and isCompleted.

CoroutineScope It is defined a simple factory function that takes CoroutineContexts as arguments to create wrapper around the combined CoroutineContext as

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    // CoroutineScope is used intentionally for user-friendly representation
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}

and creates a Job element if the provide context does not have one already.

Let's look at GlobalScope source code

/**
 * A global [CoroutineScope] not bound to any job.
 *
 * Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
 * and are not cancelled prematurely.
 * Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
 *
 * Application code usually should use an application-defined [CoroutineScope]. Using
 * [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
 * on the instance of [GlobalScope] is highly discouraged.
 *
 * Usage of this interface may look like this:
 *
 * ```
 * fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = GlobalScope.produce(Dispatchers.Unconfined) {
 *     for (number in this) {
 *         send(Math.sqrt(number))
 *     }
 * }
 * ```
 */
public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

As you can see it extends CoroutineScope

1- GlobalScope.launch(Dispatchers.IO) {} GlobalScope is alive as long as you app is alive, if you doing some counting for instance in this scope and rotate your device it will continue the task/process.

GlobalScope.launch(Dispatchers.IO) {} 

runs as long as your app is alive but in IO thread because of using Dispatchers.IO

2- GlobalScope.launch{} It's same as the first one but by default, if you don't have any context, launch uses EmptyCoroutineContext which uses Dispatchers.Default, so only difference is thread with first one.

3- CoroutineScope(Dispatchers.IO).launch{} This one is the same as first one with only syntax difference.

4- lifecycleScope.launch(Dispatchers.IO){} lifecycleScope is an extention for LifeCycleOwner and bound to Actvity or Fragment's lifCycle where scope is canceled when that Activity or Fragment is destroyed.

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

You can also use this as

class Activity3CoroutineLifecycle : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main + CoroutineName(" Activity Scope") + CoroutineExceptionHandler { coroutineContext, throwable ->
            println(" Exception $throwable in context:$coroutineContext")
        }


    private val dataBinding by lazy {
        Activity3CoroutineLifecycleBinding.inflate(layoutInflater)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(dataBinding.root)
    
        job = Job()

        dataBinding. button.setOnClickListener {

            // This scope lives as long as Application is alive
            GlobalScope.launch {
                for (i in 0..300) {
                    println(" Global Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
                    delay(300)
                }
            }

            // This scope is canceled whenever this Activity's onDestroy method is called
            launch {
                for (i in 0..300) {
                    println(" Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
                    withContext(Dispatchers.Main) {
                        dataBinding.tvResult.text = " Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this"
                    }
                    delay(300)
                }
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }

}
VIN
  • 6,385
  • 7
  • 38
  • 77
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 1
    "CoroutineExceptionHandler — handles uncaught exceptions" -- this comes with many caveats, users fall for many traps. Only the exception handler in the top-level coroutine has any effect, and only if it's in a `launch`ed coroutine, as opposed to `async`, where it's ignored. – Marko Topolnik Nov 30 '20 at 10:15
  • 2
    "Dispatchers.Main: recommended dispatcher for performing UI-related events. " -- not just recommended, but _mandatory_. Not using it to work with the GUI causes the app to crash. – Marko Topolnik Nov 30 '20 at 10:16
  • @MarkoTopolnik, right any View tree related UI operations it's mandatory to do in UI thread should be done with Dispatchers.Main or Dispatchers.Main.immediate. This is only true for operations you normally do Android's main or GUI thread. You can still use other threads with SurfaceView or other elements that can do UI operations. I use Dispatchers.Default with SurfaceViews. – Thracian Nov 30 '20 at 10:28
  • "this comes with many caveats, users fall for many traps. Only the exception handler in the top-level coroutine has any effect, and only if it's in a launched coroutine, as opposed to async, where it's ignored.", didn't say it has any caveats, but the definition on Kotlin official page is "CoroutineExceptionHandler is invoked only on uncaught exceptions — exceptions that were not handled in any other way. " – Thracian Nov 30 '20 at 10:30
  • About the exception handler, the docs say `Normally, uncaught exceptions can only result from root coroutines created using the launch builder.` And then a bunch more content follows that you need to understand because it's very easy to get the wrong idea on how to use this feature. – Marko Topolnik Nov 30 '20 at 13:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225321/discussion-between-thracian-and-marko-topolnik). – Thracian Nov 30 '20 at 14:00
  • Is third really similar to the first one? with just syntactical differences? – Saurabh Kumar Nov 02 '21 at 16:43
  • 1
    @luG_0 no, it's not. Third one is alive as long as application is. You can copy past the snippet that contains `Globalscope` and try it yourself. When you rotate the screen you will see that it's still running. – Thracian Nov 02 '21 at 17:07
  • Oops can you update the same in your answer? I got confused reading that. Thanks !! – Saurabh Kumar Nov 02 '21 at 17:11
  • If we are to get technical, the third one *behaves* the same as the first, but it's really creating a new scope object. It behaves the same because we aren't storing the created scope in a property and managing its lifecycle, so we are treating it as if it were a GlobalScope. – Tenfour04 Jan 24 '23 at 14:20
  • @Thracian Hi can you please help me on this [issue](https://stackoverflow.com/q/75367786/8266651) – Kotlin Learner Feb 07 '23 at 15:31
42

TL;DR

  1. GlobalScope.launch(Dispatchers.IO): Launches a top-level coroutine on Dispatchers.IO. Coroutine is unbound and keeps running until finished or cancelled. Often discouraged since programmer has to maintain a reference to join() or cancel().

  2. GlobalScope.launch: Same as above, but GlobalScope uses Dispatchers.Default if not specified. Often discouraged.

  3. CoroutineScope(Dispatchers.IO).launch: Creates a coroutine scope which uses Dispatchers.IO unless a dispatcher is specified in the coroutine builder i.e. launch

  4. CoroutineScope(Dispatchers.IO).launch(Dispatchers.Main): Bonus one. Uses the same coroutine scope as above (if the scope instance is same!) but overrides Dispatcher.IO with Dispatchers.Main for this coroutine.

  5. lifecycleScope.launch(Dispatchers.IO): Launches a coroutine within the lifecycleScope provided by AndroidX. Coroutine gets cancelled as soon as lifecycle is invalidated (i.e. user navigates away from a fragment). Uses Dispatchers.IO as thread pool.

  6. lifecycleScope.launch: Same as above, but uses Dispatchers.Main if not specified.

Explanation

Coroutine scope promotes structured concurrency, whereby you can launch multiple coroutines in the same scope and cancel the scope (which in turn cancels all the coroutines within that scope) if the need be. On the contrary, a GlobalScope coroutine is akin to a thread, where you need to keep a reference in-order to join() or cancel() it. Here's an excellent article by Roman Elizarov on Medium.

CoroutineDispatcher tells the coroutine builder (in our case launch {}) as to which pool of threads is to be used. There are a few predefined Dispatchers available.

  • Dispatchers.Default - Uses a thread pool equivalent to number of CPU cores. Should be used for CPU bound workload.
  • Dispatchers.IO - Uses a pool of 64 threads. Ideal for IO bound workload, where the thread is usually waiting; maybe for network request or disk read/write.
  • Dispatchers.Main (Android only): Uses main thread to execute the coroutines. Ideal for updating UI elements.

Example

I've written a small demo fragment with 6 functions corresponding to the above 6 scenarios. If you run the below fragment on an Android device; open the fragment and then leave the fragment; you'll notice that only the GlobalScope coroutines are still alive. Lifecycle coroutines are cancelled by lifecycleScope when the lifecycle is invalid. On the other hand, CoroutineScope ones are cancelled on onPause() invocation which is explicitly done by us.

class DemoFragment : Fragment() {

    private val coroutineScope = CoroutineScope(Dispatchers.IO)

    init {
        printGlobalScopeWithIO()
        printGlobalScope()
        printCoroutineScope()
        printCoroutineScopeWithMain()
        printLifecycleScope()
        printLifecycleScopeWithIO()
    }

    override fun onPause() {
        super.onPause()
        coroutineScope.cancel()
    }

    private fun printGlobalScopeWithIO() = GlobalScope.launch(Dispatchers.IO) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[GlobalScope-IO] I'm alive on thread ${Thread.currentThread().name}!")
        }
    }

    private fun printGlobalScope() = GlobalScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[GlobalScope] I'm alive on ${Thread.currentThread().name}!")
        }
    }
    
    private fun printCoroutineScope() = coroutineScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[CoroutineScope] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[CoroutineScope] I'm exiting!")
    }

    private fun printCoroutineScopeWithMain() = coroutineScope.launch(Dispatchers.Main) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm exiting!")
    }

    private fun printLifecycleScopeWithIO() = lifecycleScope.launch(Dispatchers.IO) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[LifecycleScope-IO]  I'm exiting!")
    }

    private fun printLifecycleScope() = lifecycleScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[LifecycleScope] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[LifecycleScope] I'm exiting!")
    }

}
Antimonit
  • 2,846
  • 23
  • 34
Siddharth Kamaria
  • 2,448
  • 2
  • 17
  • 37
24

I'd organize your list along three axes:

  1. GlobalScope vs. CoroutineScope() vs. lifecycleScope
  2. Dispatchers.IO vs. inherited (implicit) dispatcher
  3. Specify the dispatcher in the scope vs. as an argument to launch

1. Choice of Scope

A big part of Kotlin's take on coroutines is structured concurrency, which means all the coroutines are organized into a hierarchy that follows their dependencies. If you're launching some background work, we assume you expect its results to appear at some point while the current "unit of work" is still active, i.e., the user hasn't navigated away from it and doesn't care anymore about its result.

On Android, you have the lifecycleScope at your disposal that automatically follows the user's navigation across UI activities, so you should use it as the parent of background work whose results will become visible to the user.

You may also have some fire-and-forget work, that you just need to finish eventually but the user doesn't await its result. For this you should use Android's WorkManager or similar features that can safely go on even if the user switches to another application. These are usually tasks that synchronize your local state with the state kept on the server side.

In this picture, GlobalScope is basically an escape hatch from structured concurrency. It allows you to satisfy the form of supplying a scope, but defeats all the mechanisms it's supposed to implement. GlobalScope can never be cancelled and it has no parent.

Writing CoroutineScope(...).launch is just wrong because you create a scope object without a parent that you immediately forget, and thus have no way of cancelling it. It's similar to using GlobalScope but even more hacky.

2. Choice of Dispatcher

The coroutine dispatcher decides which threads your coroutine may run on. On Android, there are three dispatchers you should care about:

  1. Main runs everything on the single GUI thread. It should be your main choice.
  2. IO runs the coroutine on a special, flexible thread pool. It exists only as a workaround when you are forced to use a legacy, blocking IO API that would block its calling thread.
  3. Default also uses a thread pool, but of fixed size, equal to the number of CPU cores. Use it for computation-intensive work that would take long enough to cause a glitch in the GUI (for example, image compression/decompression).

3. Where to Specify the Dispatcher

First, you should be aware of the dispatcher specified in the coroutine scope you're using. GlobalScope doesn't specify any, so the general default is in effect, the Default dispatcher. lifecycleScope specifies the Main dispatcher.

We already explained that you shouldn't create ad-hoc scopes using the CoroutineScope constructor, so the proper place to specify an explicit dispatcher is as a parameter to launch.

In technical detail, when you write someScope.launch(someDispatcher), the someDispatcher argument is actually a full-fledged coroutine context object which happens to have a single element, the dispatcher. The coroutine you're launching creates a new context for itself by combining the one in the coroutine scope and the one you supply as a parameter. On top of that, it creates a fresh Job for itself and adds it to the context. The job is a child of the one inherited in the context.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • Very good answer. I always saw Dispatchers.IO thread to be used for REST and DB operations in every source. And ROOM for instance uses ArchDiskIO thread for operations suspend, i haven't checked out which thread name Retrofit uses. Would you mind sharing a source for Dispatcher.IO. Checked out official document but couldn't find it there either. It would be welcome. And my other question which Dispatchers should we choose, actually they get shared when you call Dispatcher.Default or IO but still when using File api? – Thracian Nov 30 '20 at 10:37
  • [Official Android Documents](https://developer.android.com/kotlin/coroutines-adv?hl=de#main-safety)**Dispatchers.IO** - This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations. – Thracian Nov 30 '20 at 10:43
  • 1
    Yeah, there's actually a lot of outdated docs like that on the Android site. Room has actually had first-class `suspend fun` support for almost [two years](https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5) now. Similar for Retrofit and I guess all the others by now. The simple rule of thumb is that if you use `Dispatchers.IO` to call a `suspend fun`, you're doing it wrong. – Marko Topolnik Nov 30 '20 at 13:52
  • 1
    @MarkoTopolnik, So now should I change launch(IO)/withContext(IO) to launch(Main)/withContext(Main)? Even which conatin database queries? – Sourav Kannantha B Apr 16 '21 at 16:10
  • @SouravKannanthaB Yes, and that's how coroutines are meant to work in the first place, whereas `withContext(IO)` is only a necessary workaround to deal with blocking code. As long as you use a library that lets you talk to the database by calling `suspend fun`s, you have no reason to use a separate threadpool. – Marko Topolnik Apr 17 '21 at 10:41
  • @MarkoTopolnik coroutines launched with `lifecycleScope` get cancelled when the lifecycle of the activity ends. But if I asynchronously make a write to database, then it must not get cancelled when the activity ends. In this case, I suppose `CoroutineScope(IO)` works properly. – Sourav Kannantha B Apr 18 '21 at 08:02
  • 1
    @SouravKannanthaB While in the previous comment you addressed the choice of dispatcher, now you have redirected the discussion to the choice of scope. These are two unrelated concerns. If you use `suspend fun`s to talk to the DB, no need for the IO dispatcher. If you perform DB ops in the background, you can try using `GlobalScope.launch`, but that's unsafe because Android may kill your process at any time. The proper way to submit background work is through the `WorkManager`. In no case do you need the `CoroutineScope(IO).launch` idiom, which is identical to `GlobalScope.launch(IO)`. – Marko Topolnik Apr 18 '21 at 08:57
  • @MarkoTopolnik. Ok. So no multithreading? Is this how it should be?? – Sourav Kannantha B Apr 18 '21 at 13:52
  • @SouravKannanthaB Exactly, that's the point of coroutines on Android. Concurrency on a single thread. – Marko Topolnik Apr 18 '21 at 17:59
  • Seems weird.. Can you point me towards an article reasoning this. – Sourav Kannantha B Apr 19 '21 at 14:43
  • @SouravKannanthaB https://medium.com/android-news/kotlin-coroutines-threads-concurrency-and-parallelism-101-78a56e09d373 – Marko Topolnik Apr 20 '21 at 05:56
8

You should know that if you want to launch suspend function you need to do it in CoroutineScope. Every CoroutineScope have CoroutineContext. Where CoroutineContext is a map that can contain Dispatcher (dispatches work to the appropriate thread), Job (controls the lifecycle of the coroutine), CoroutineExceptionHandler (handles uncaught exceptions), CoroutineName (name of the coroutine, useful for debugging).

  1. GlobalScope.launch(Dispatchers.IO) {} - GlobalScope.launch creates global coroutines and using for operations that should not be canceled, but a better alternative would be creating a custom scope in the Application class, and inject it to the class that needs it. This has the advantage of giving you the ability to use CoroutineExceptionHandler or replace the CoroutineDispatcher for testing.
  2. GlobalScope.launch{} - same as GlobalScope.launch(Dispatchers.IO) {} but runs coroutines on Dispatchers.Default.Dispatchers.Default is a default Dispatcher that is used if no dispatchers is specified in their context.
  3. CoroutineScope(Dispatchers.IO).launch{} - it's create scope with one parameter and launch new coroutine in it on IO thread. Will be destroyed with object where it was launched. But you should manually call .cancel() for CoroutineScope if you want to end your work properly.
  4. lifecycleScope.launch(Dispatchers.IO){} - it is existing scopes that available from a Lifecycle or from a LifecycleOwner (Activity or Fragment) and comes in your project with dependency androidx.lifecycle:lifecycle-runtime-ktx:*. Using it you can rid from manualy creating CoroutineScope. It will run your job in Dispatchers.IO without blocking MainThread, and be sure that your jobs will be cancelled when your lifecycle is destroyed.
  5. lifecycleScope.launch{} - same as lifecycleScope.launch(Dispatchers.IO){} that create CoroutinesScope for you with default Dispatchers.Main parameter and runs your coroutines in Dispatcher.Main that mean you can work with UI.
i30mb1
  • 3,894
  • 3
  • 18
  • 34