Learning Coroutine
7 Gotchas When Explore Kotlin Coroutine
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.
Note: this is assuming you have some Coroutine basic knowledge. If you don’t check out the below first.
1. Differentiating Thread and Coroutine
2. Understanding Suspend Function
3. Kotlin Coroutine Scope, Context and Job
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.activity_main)
runBlocking(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.activity_main)
runBlocking {
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
}
So that means, runBlocking
in the main thread is not the same as runBlocking(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() {
runBlocking {
println(Thread.currentThread())
println(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() {
runBlocking(Dispatchers.Main) {
println(Thread.currentThread())
println(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.activity_main) runBlocking(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() {
runBlocking(CoroutineName("My Coroutine")) {
println("${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.activity_main) runBlocking(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
- it is not blocking
- it is on a different thread
- 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. (note if the logging happens at different internal time, it will still be printed).
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.