Some time ago I did an introduction to Kotlin’s Coroutines. While in its early stages, coroutines is taking over the Android Dev ecosystem as a new lightweight method of asynchronous programming which already gives it an edge against its Reactive counterpart.
Within coroutines are 2 important concepts: Context and Scopes. To be able to use coroutines to their fullest extent, we need to grasp complete hold of these concepts.
The Scope of a coroutine defines ‘where it belongs’. This is used to handle batches of coroutines during key lifecycle events. For example, coroutines can belong to an Activity Scope, or an Global Application Scope.
To build the scope, you need a Context which contains various elements that coroutines make use of, the main ones being the dispatcher (defines which thread a coroutine is performed on) and the job (a single task or parent of a whole set of tasks).
How are they used
var mJob = Job() var viewModelScope = CoroutineScope(Dispatchers.IO + mJob)
You wouldn’t normally define the Coroutine Context standalone, but instead, as part of the Scope’s constructor. In this case, the context is Dispatchers.IO + mJob.
override fun onDestroy() { uiScope.coroutineContext.cancel() super.onDestroy() }
Now our coroutines are set to run on the IO thread and are tied to mJob as their parent job. Now we can cancel all coroutines tied to this scope in a single line of code.
Checking Job State
In addition to cancelling jobs, while jobs are being executed, you can call any of its boolean properties to check its status:
isActive – True when the job has started but is not yet complete or cancelled
isComplete – True when a job has either been cancelled or completed with success or failure
isCancelled – True when a job is.. well… cancelled
Jobs can also perform attachChild(), join(), and start() methods, although more on them another day.
CoroutineContext.cancel() vs Job.cancel
uiScope.coroutineContext.cancel() mJob.cancel()
What’s the difference between these 2 lines of code? They both cancel all coroutines within the scope yeah? Well.. yeah. They do. In this case, they do the exact same thing. CoroutineContext.cancel() accesses the main job tied to the context and calls cancel() on it.
Dispatchers and Threads
I listed these already in my introduction post, but for reference and refreshers:
Dispatchers.Main – Runs on the Main/UI thread and thus, is the only thread where UI changes can be made without exception. Overpopulating this thread may decrease the app’s front-end performance and freeze the app, resulting in bad UX.
Dispatchers.Default – Appropriate for CPU intensive tasks. Backed by a shared pool of threads and limited by the number of CPU cores.
Dispatchers.IO – Best used for non-CPU intensive background tasks like network and database calls. Also backed by a shared pool of threads.
Dispatchers.Unconfined – Runs coroutines unconfined to any specific thread. Unconfined coroutines start in the current thread and resumes in any thread switched to in the coroutine function. (Ex. Calling delay will switch to the Default scheduler, thus the coroutine will resume there, even if it was started in the Main thread).
Creating a Job Hierarchy
We already know that a “main job” is tied to the context. Calling any operations (such as cancel) on this job will apply to all jobs and coroutines below it i.e. its children. That being said, we can build a whole hierarchy of jobs.
val mJob = Job() val networkJob = Job(mJob) val uiJob = Job(mJob) val uiScope = CoroutineScope(Dispatchers.Main + uiJob) val networkScope = CoroutineScope(Dispatchers.IO + networkJob)
In order for coroutines to make use of them, we need to associate jobs with a scope. Here, uiScope and networkScope are both children of mJob. Cancelling mJob will cancel both of them, but more importantly, I can call coroutines on either the Main thread or the IO thread with these two separately defined scopes.
Global Scope and why it’s bad
The GlobalScope is a pre-defined scope that can be accessed anywhere within the application. As such, coroutines in the GlobalScope run in the background across the whole application thus, activity lifecycles don’t affect it.
Immediately you might be thinking “well I can think of a few uses for this”. Well, even the official docs says use of functions on the GlobalScope is highly discouraged. Let me tell you why it’s a pretty disgusting move.
Firstly, there is NO structured concurrency within the GlobalScope. There’s no hierarchy with jobs, no parent-child relationships. All coroutines executed here are independent of each other. On top of this, the GlobalScope never gets destroyed until:
- The JVM shuts down
- The class is unloaded
- The process dies
Therefore, if you have a coroutine that is working for a really long time or perhaps indefinitely, you’re going to have a real performance vampire in the background of your application. Coroutines themselves are lightweight but the tasks they are performing may not be.
On top of this, if one of your coroutines fails with an exception, all jobs currently running on the GlobalScope will leak and keep executing, potentially indefinitely. If they start failing as well, you may not even know about it.
Thus, you as a developer have to keep track of every coroutine running on the GlobalScope which is much harder than keeping track of the scope as a whole.
If you need to make use of an application-wide scope, the better solution is to define your own application-defined scope which can be accessed from anywhere in your application.