Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parallel decomposition of independent async tasks with kotlin coroutines

I am trying to run multiple tasks in parallel, but the tasks are independent so if one of children coroutine fails, I do not want its siblings or parent to fail too. In the code below I have used coroutineScope to create a new scope in which these tasks run and launched 5 async tasks each on sending its id and the delay time it should wait. The second coroutine throws an exception. In this case the code does what i want it to do, it calculates the sum of the jobs that successfully finished and the ones that failed return 0.

However, I am reading that there is also supervisorScope in the kotlinx library which should be preferred instead of coroutineScope (which cancels parent/siblings if exception is not handled) for tasks are not dependent on others. I am not sure why I should be changing to use supervisorScope as I am getting the result I want with coroutineScope.

Q1: If i were to change to supervisorScope should something change in my async blocks?

Q2: Is it accepted to catch any exception inside the async block and dont let anything be propagated to its parent? I know you can also catch exceptions during .await() phase but is that the way it should be done?

runBlocking {
    coroutineScope {
            val job1 = async<Int> {
                try {
                    request(1, 1000)
                } catch (e: Exception) {
                    println("Job 1 failed with $e")
                    0
                }
            }

            val job2 = async<Int> {
                try {
                    request(2, 2000)
                    throw Exception("cancelling Job 2")
                } catch (e: Exception) {
                    println("Job 2 failed: $e")
                    0
                }
            }

            val job3 = async {
                try {
                    request(3, 3000)
                } catch (e: Exception) {
                    println("Job 3 failed with $e")
                    0
                }
            }

            val job4 = async {
                try {
                    request(4, 4000)
                } catch (e: Exception) {
                    println("Job 4 failed with $e")
                    0
                }
            }

            val job5 = async {
                try {
                    request(5, 5000)
                } catch (e: Exception) {
                    println("Job 5 failed with $e")
                    0
                }
            }

            val result = job1.await() + job2.await() + job3.await() + job4.await() + job5.await()
            println(result.toString())
        }

        println("Finished")
}

suspend fun request(id: Int, time: Long): Int {
    println("Job $id started")
    delay(time)
    println("Job $id finished")
    return id
}
like image 627
chaosmonk Avatar asked Nov 26 '25 06:11

chaosmonk


1 Answers

The reason all coroutines run to completion is that you catch the exception thrown by job 2 within job 2 itself, so it never propagates up the hierarchy of Jobs, so nothing happens.

However, if you remove that catch clause in job2, job[1-5] will always be canceled, independent of if you use coroutineScope or supervisorScope.

This is because job2.await() will throw the exception instead. Since this happens within the parent job of job[1-5] (i.e. within the top coroutineScope/supervisorScope), and since a failed/cancelled parent job always cancel child jobs, job[1-5] will also be cancelled.

A1: Use none of coroutineScope or supervisorScope, remove coroutineScope and put things directly under runBlocking.

A2: It certainly is allowed to catch exceptions within async { } to make sure it doesn't happen in .await() if it fits your use case.

like image 65
Enselic Avatar answered Nov 28 '25 02:11

Enselic



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!