Learning Coroutine

7 Gotchas When Explore Kotlin Coroutine

Little aha moments when learning Kotlin Coroutine

Elye
Elye
Jan 3 · 7 min read
Image for post
Image for post
Photo by Alex Sheldon on Unsplash

I took my year-end break to explore more on Kotlin Coroutine Ecosystem. I’m surprised to find out many unexpected behaviors. Some are documented (I found them later), and some are not (or at least I haven’t found them).

To get to the answer, I post on StackOverflow, pull my hair, take a nap to get some idea on how to debug to find the answer.

Sharing here for the benefits of all.

1. runBlocking can hang your App.

Try out the code, just run it in a simple empty app.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.)

(Dispatchers.Main) {
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
}

It will hang your App, not able to finish the onCreate function call. Instead, if you run the below it will work fine. (removing the Dispatchers.Main)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.)

{
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
}

So that means, in the main thread is not the same as (Dispatchers.Main) in the main thread!!

2. runBlocking in the main thread is not the same as runBlocking(Dispatchers.Main)

That’s so confusing to me initially.

To me, I always think that when we trigger runBlocking in the main thread, it will be using Dispatchers.Main. Apparently, it is not.

To further prove this, let’s run this in the test instead.

@Test
fun running() {
{
(Thread.currentThread())
(coroutineContext)
}
}

This should be fine printing

Thread[main @coroutine#1,5,main]
[CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@436a4e4b, BlockingEventLoop@f2f2cc1]

But if we run

@Test
fun running() {
(Dispatchers.Main){
(Thread.currentThread())
(coroutineContext)
}
}

It will crash with

Java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize

To fix this, we will need to set Dispatchers.Main Delegate. After doing this, when we run, it will print out

Thread[Test Main @coroutine#2,5,main]
[CoroutineId(2), "coroutine#2":BlockingCoroutine{Active}@20f637a1, Dispatchers.Main]

Clearly, the thread name is different from the one runBlocking. i.e. Dispatchers.Main vs. BlockingEventLoop@f2f2cc1.

Check this StackOverflow for more info.

3. Android cannot print coroutine id name by default in Thread.currentTread()

For debugging purposes, we can assign a coroutine with a name using CoroutineName context. And by logging it out Thread.currentThread(), it will show the name.

However, when trying to Log out on Android, it doesn’t print the name out.

Run the following code

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.)
(CoroutineName("My Coroutine")) {
Log.d("Track", "${Thread.currentThread()}")
}
}

It just prints out the following.

Thread[main,5,main]

We have the thread name, priority, thread group.

If we print it in the test environment as below

@Test
fun running() {
(CoroutineName("My Coroutine")) {
("${Thread.currentThread()}")
}
}

It will print

Thread[main @My Coroutine#1,5,main]

Notice @My Coroutine#1 is the coroutine’s name and the ID.

Solution

In order to get Android to be able to print the name, one option is to print the coroutineContext instead.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.)
(CoroutineName("My Coroutine")) {
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
}

It will print the below

Thread[main,5,main][CoroutineName(My Coroutine), BlockingCoroutine{Active}@c3e0260, BlockingEventLoop@1607319]

The other option is to set the kotlinx.coroutines.debug, which can be done in the Application.

System.setProperty("kotlinx.coroutines.debug", "on" )

With this, it will print

Thread[main @My Coroutine#1,5,main][CoroutineName(My Coroutine), CoroutineId(1), "My Coroutine#1":BlockingCoroutine{Active}@c3e0260, BlockingEventLoop@1607319]

4. Coroutine operation cannot be canceled at any time

Try the following code.

runBlocking {
println("Launching...")
val job = launch(Dispatchers.IO) {
repeat(2000) {
repeat(2000) {
println("Suspending...")
}
}
println("Done...")
}
println("Launched...")
delay(100)
println("Canceling...")
job.cancel()
println("Canceled...")
}

You’ll notice that the coroutine the output as below.

Track: Launching...
Track: Launched...
Track: Suspending...
Track: Suspending...
Track: Canceling...
Track: Suspending...
Track: Suspending...
Track: Canceled...
Track: Suspending...
Track: Suspending...
Track: Suspending...
Track: Suspending...
:
: (a lot more suspending)
:
Track: Done...

Even though

  1. it is not blocking
  2. it is on a different thread
  3. the cancel has been triggered when it is running.

it won’t get canceled after 0.1 seconds. Instead, it will run and complete the long loop until Track: Done....

Explanation

In coroutine, the suspension and cancellation only happen during the suspend -function (e.g. yield(), delay()).

The cancelation is cooperative, as mentioned in Kotlin Documentation. We’ll need to have either yield() or check for isActive.

To have a better understanding of when coroutine suspend, terminate (cancel), or started, refers to the below article.

Given that cancellation can’t terminate the process immediately, how should it be handled in Network Fetching? You can check out the below article.

If you have further input on this, feel free to post it in this StackOverflow.

5. Why without delay, Android coroutine doesn’t finish its task

I have a code as below

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking {
launch {
repeat(5) {
Log.d("Track", "First, current thread")
delay(1)
}
}
launch {
repeat(5) {
Log.d("Track", "Second, current thread")
delay(1)
}
}
}
}

It will result as below, where each coroutine alternating the run until completion of 5 times.

Track: First, current thread
Track: Second, current thread
Track: First, current threa
Track: Second, current thread
Track: First, current thread
Track: Second, current thread
Track: First, current thread
Track: Second, current thread
Track: First, current thread
Track: Second, current thread

But if we run this in Android as below (remove delay(1))

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking {
launch {
repeat(5) {
Log.d("Track", "First, current thread")
// delay(1)
}
}
launch {
repeat(5) {
Log.d("Track", "Second, current thread")
// delay(1)
}
}
}
}

First, it doesn’t alternate anymore. This is expected as there’s no yield and delay for the function to suspend.

Second, it will only run twice and move on to the other coroutine. It’s so strange. Why is it so?

Track: First, current thread
Track: First, current thread
Track: Second, current thread
Track: Second, current thread

Explanation

Apparently, Android will automatically remove 3rd and subsequent logging when there are more than 2 exact same messages printed at the same time. .

This is not a coroutine issue, but Android instead.

Share it here, as I found out when working on Coroutine trying to understand the effect of yield (or delay) and without it

Refer to this StackOverflow.

6. Can we reuse a canceled coroutine scope?

Run the following

@Test
fun testingLaunch() {
val scope = MainScope()
runBlocking {
scope.cancel()
scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}.join()
println("Finished")
}
}

You notice the scope.launch doesn’t work anymore.

Similarly, for below,

@Test
fun testingAsync() {
val scope = MainScope()
runBlocking {
scope.cancel()
val defer = scope.async {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
defer.await()
println("Finished")
}
}

Not only it doesn’t run properly, it will crash during defer.await()

kotlinx.coroutines.JobCancellationException: Job was cancelled
; job=SupervisorJobImpl{Cancelled}@39529185

Remove the scope.cancel() from the above code make it works.

No, we cannot use the scope again after it is canceled.

From my experiment, can a scope has been canceled, we can no longer get it launch. There’s no way to reset it.

I post this in the StackOverflow here on some discussion. Feel free to respond to it if you find otherwise.

The only way to solve it is, after a scope has been canceled, create a new one for a new process.

7. Default coroutine scope cannot re-launch during exception handle

In Coroutine, we have CoroutineExceptionHandler to simply the exception capture as we show below.

private var coroutineScope: CoroutineScope? = nullprivate val errorHandler = CoroutineExceptionHandler { 
context, error ->
println("Launch Exception ${Thread.currentThread()}")
coroutineScope?.launch(Dispatchers.Main) {
println("Launch Exception Result ${Thread.currentThread()}")
}
}

@Test
fun testData() {
runBlocking {
coroutineScope = CoroutineScope(Dispatcher.IO)
coroutineScope?.launch(errorHandler) {
println("Launch Fetch Started ${Thread.currentThread()}")
throw IllegalStateException("error")
}?.join()
}
}

This experiment above will throw an exception to see if it can be captured by CoroutineExceptionHandler. The result is as below.

Launch Fetch Started Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]
Launch Exception Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]

The exception is captured by CoroutineExceptionHandler, but the below is not launched

coroutineScope?.launch(Dispatchers.Main) {
println("Launch Exception Result ${Thread.currentThread()}")
}

It made me puzzled for a while, as per this StackOverflow.

We need SupervisorJob() CoroutineScope to handle it

Apparently, the default CoroutineScope(Dispatchers.IO) will no longer function properly when the child coroutine met with exception.

To decouple the exception of the child from its parent, we’ll need to use SupervisorJob() in the CoroutineScope. This is mentioned in SupervisorJob.

So changing the code to the below will make it works.

private var coroutineScope: CoroutineScope? = nullprivate val errorHandler = CoroutineExceptionHandler { 
context, error ->
println("Launch Exception ${Thread.currentThread()}")
coroutineScope?.launch(Dispatchers.Main) {
println("Launch Exception Result ${Thread.currentThread()}")
}
}

@Test
fun testData() {
runBlocking {
coroutineScope = CoroutineScope(
SupervisorJob() + Dispatcher.IO)
coroutineScope?.launch(errorHandler) {
println("Launch Fetch Started ${Thread.currentThread()}")
throw IllegalStateException("error")
}?.join()
}
}

The result is now

Launch Fetch Started Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]
Launch Exception Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main]
Launch Exception Result Thread[Test Main @coroutine#3,5,main]

Hopes this sharing remove some roadblock from your learning and experimenting Kotlin Coroutine.

Mobile App Development Publication

Sharing Mobile App Development and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store