Saturday, June 6, 2020

How to Stink at Coroutines

The main sell of Kotlin Coroutines is that they enforce a simple structured concurrency. Have a concurrent job that launches many concurrent jobs in parallel and waits for them all to finish before continuing? Coroutines make it easy - until they don't because you are misunderstanding how coroutines work and are wondering why your coroutines continue to run.

Let's observe the following code:

class MyViewModel : ViewModel() {

  fun mainTask(): Job {
    return viewModelScope.launch {
      doSubTaskAndCancelWhenMainCancels()
    }
  }

  fun doSubTaskAndCancelWhenMainCancels() {
    viewModelScope.launch {
      doWork()
    }
  }

}

val job = myViewModelInstance.mainTask()
...
job.cancel()

The mainTask uses the viewModelScope to launch a job, and that main job launches another job as a subtask. It may launch many subtasks, though for this example we just use one. Now - normally, the scope would be cancelled entirely in the ViewModel onCleared(), but let's say that this view model is Activity scoped but currently being used in a Fragment - the onCleared will not be called, so you must cancel this task manually when the fragment is destroyed to properly stop it.

Do you see what is wrong here? Because up until about 15 minutes ago I didn't.

The mainTask launches a job. The subtask launches a job. Though it appears that the subtask is scoped to the mainTask scope due to how it is launched, because we call the subtask via its own viewModelScope.launch {} call, it actually is completely independent of the mainTask. Thus, even when we cancel the mainTask, the subtask will continue to run until it is completed. This can create an issue, when for example the sub tasks are listening to event busses which expect to stop listening once the fragment is destroyed. Our concurrency invocations are not structed here - they are simply invoked in a specific order. To make them truly structured and have this code perform the way one would expect, you must change the subtask function.

class MyViewModel : ViewModel() {

  fun mainTask(): Job {
    return viewModelScope.launch {
      doSubTaskAndCancelWhenMainCancels()
    }
  }

  fun CoroutineScope.doSubTaskAndCancelWhenMainCancels() {
    this.launch {
      doWork()
    }
  }

}

val job = myViewModelInstance.mainTask()
...
job.cancel()

By making the subtask an extension function of a CoroutineScope, this enforces a structured concurrency. In fact, this is one of the first things the Kotlin Coroutine page tells you about how coroutines work - its a beginner mistake really - but one that may at first glance seem non obvious and so bears repeating. The only way to enforce the concurrency of subtasks is to launch those coroutine subtasks using the parent's CoroutineScope, and the easiest way to avoid shooting yourself in the foot is to make these subtasks extensions of the CoroutineScope so that you will never be able to call them in the wrong context.

On a related note - PYDroid 21.0.5 is out and fixes the bugs introduced by 21.0.4 which was introduced to fix the bugs in 21.0.3. Please use 21.0.5 as its API surface is the same as 21.0.3 but has some very critical bugs fixed.

========================
Follow pyamsoft around the Web for updates and announcements about the newest applications!
Like what I do?

Send me an email at: pyam.soft@gmail.com
Or find me online at: https://pyamsoft.blogspot.com

Follow my Facebook Page
Check out my code on GitHub
=========================