Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Hyundai Motor Group on Unsplash

When I first started learning Kotlin Coroutines and its dispatchers, I was more in the mindset of learning how to use multi-thread in Kotlin, and Dispatchers being just a way to mark the right thread for the job. But a couple more years in as a full-time Android developer, this partial understanding of Kotlin Coroutines Dispatchers soon met its limits. So today, I would like to lead you deep into Kotlin Coroutines Dispatchers myself through Dispatchers.Main.immediate as a prominent example.

The Basics: Dispatching Coroutines to the Right Thread.

As we all know, when launching a new coroutine, the dispatcher dispatches the given coroutine to the right thread (in this case the Main thread). Typically this is done using the message queue and as a result, the order in which coroutines are run depends on the order they were queued.

fun main() {
  print(1)
  CoroutineScope(Dispatchers.Main).launch {
    print(2)
  }
  print(3)
}

/* output: 132 */

In the above example, initially, the main thread is occupied by the main() function. When launch() is called this coroutine is sent to the message queue, waiting for the main thread to be ready to handle another message. So “2” won’t be printed after printing “1” and will have to wait until main() finishes. As main() prints “3” and finishes, the launched coroutine is picked up by the looper to run, thus finally printing “2”.

To dispatch or not to dispatch

However, even before all this dispatching, the dispatcher is responsible for determining whether a dispatch should happen in the first place by providing the method isDispatchNeeded(). In the case of skipping the dispatch, that coroutine skips the message queue and is run immediately.

fun main() {
  print(1)
  CoroutineScope(Dispatchers.Main.immediate).launch {
    print(2)
  }
  print(3)
}

/* output: 123 */

Since Dispatchers.Main.immediate returns false for isDispatchNeeded() while on the main thread, the coroutine runs sequentially, just like any other function would, without being sent to the message queue.

Dispatching on resume

Additionally, every time a coroutine resumes from a suspend state it also must run the process of dispatching (including checking whether or not to dispatch at all) again to continue running. While this may seem of little importance, it can lead to results that are very hard to understand without it.

@OptIn(ExperimentalCoroutinesApi::class)// for Channel.isEmpty
fun main() {
  val channel = Channel<Int>()
  CoroutineScope(Dispatchers.Main).launch {
    val job = launch {
      for (i in channel) {
        println(i)
      }
    }

    yield()// to let the above coroutine launch and suspend at the for loop.

    channel.send(1)

    job.cancel()
    println("${channel.isEmpty}")
  }
}

/*
output:
true
*/

Channels are known to buffer the sent value until there is a receiver to receive. But in contrast to our belief, the channel.isEmpty returned true without the value “1” ever printed by the for loop on the channel. Is Channel to blame for this unexpected result? Interestingly no.

You see, in this case, the channel has successfully delivered the value to the waiting coroutine. However, since the waiting coroutine was using Dispatchers.Main (following its parent scope) it had to be dispatched to the message queue with that value. And while it was waiting to be run, the coroutine scope of that coroutine was canceled resulting in the coroutine not running even when it was out of the queue.

Then, how about Dispatchers.Main.immediate?

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

@OptIn(ExperimentalCoroutinesApi::class)// for Channel.isEmpty
fun main() {
  val channel = Channel<Int>()
  CoroutineScope(Dispatchers.Main.immediate).launch {
    val job = launch {
      for (i in channel) {
        println(i)
      }
    }

    yield()// to let the above coroutine launch and suspend at the for loop.

    channel.send(1)

    job.cancel()
    println("${channel.isEmpty}")
  }
}

/* 
output:
1
true
*/

Since Dispatchers.Main.immediate does not require a dispatch while in the main thread, when the suspended for loop was called to resume, it was able to run right where it left off.

This all may seem like a magic, how “println(i)” was able to run before “job.cancel()”. But at the same time when you look at suspend function as just a callback function with an optional thread dispatch between, it will not be so hard to understand how it was able to run the way it did.

One more queue

In spite of Dispatchers.Main.immediate skipping the message queue when possible, there is one more queue that must be considered. Those coroutines that do not need a dispatch are queued and handled by the unconfined event loop. So although it does not require an additional message for the looper, it must follow the order of the unconfined queue.

fun main() {
  val scope = CoroutineScope(Dispatchers.Main.immediate)
  
  print(1)
  scope.launch {
    print(2)
    launch {
      print(3)
    }
    launch {
      print(4)
    }
    print(5)
  }
  print(6)
}

/* output: 125346 */

Such queue and event loop is well documented in Dispatchers.Unconfined’s document. But it’s a detail that is hard to understand and easy to look over when first learning Kotlin Coroutines. And for those who are wondering why there is such a queue, according to the Dispatchers.Unconfined’s document, this queueing process is in place to avoid stack overflow.

Conclusion

In summary, Kotlin Coroutine Dispatchers do much more than just mark the thread for the coroutine to run in. Dispatchers are an important part of understanding how coroutines are run which can affect the whole outcome. Although it would be best not to depend on such race conditions, I hope this lesson gave you the knowledge needed to cope in such cases when met.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu