Managing multiple tasks with launch coroutine builder in Kotlin
In this previous article, we introduced the launch
coroutine builder to run multiple tasks concurrently. launch
starts a coroutine in a fire-and-forget fashion but suppose we want to start some tasks concurrently and wait for them to finish before starting another task.
Waiting for a Coroutine to Finish
launch
returns an instance of Job
which represents the coroutines operation and offers us the ability to start, cancel, and check its status. The join
method of Job
suspends the coroutine until the job is finished.
In the example below we start two jobs running then call join
on each of the jobs to wait for them to complete before printing whether they have completed.
suspend fun task(taskId: Int) {
println("Started task $taskId")
delay(1000)
println("Completed task $taskId")
}
fun main(): Unit = runBlocking {
val job1 = launch {
task(1)
}
val job2 = launch {
task(2)
}
job1.join()
job2.join()
// or equivalently joinAll(job1, job2)
println("Job 1 is finished: ${job1.isCompleted}")
println("Job 2 is finished: ${job2.isCompleted}")
}
Note: the joinAll(...)
function is simply jobs.forEach { it.join() }
.
This prints:
Started task 1
Started task 2
Completed task 1
Completed task 2
Job 1 is complete: true
Job 2 is complete: true
With the join
calls the coroutine execution waits for the jobs to complete resulting in the final println
calls running at the end of the program.
That's great is there anything else you can do with the Job?
Yes, you can also cancel the coroutine.
Cancellation of Coroutines
Occasionally you need very fine-grained control over a running coroutine for example cancelling a coroutine that is no longer needed. The Job
class also provides the cancel
method to, unsurprising, cancel a running coroutine.
In the example below we start a job give it some time to start then cancel it and finally print the start of the job. In this case, isCompleted
is false
and isCancelled
is true
because it was cancelled rather than completed normally.
fun main() = runBlocking {
val job = launch {
task(1)
}
delay(100) // give time to allow the coroutine to start
job.cancel()
println("Job is complete: ${job.isCompleted}")
println("Job is cancelled: ${job.isCancelled}")
}
This prints:
Started task 1
Job is complete: false
Job is cancelled: true
Note: Cancelling an already completed coroutine has no effect.
Is there a way to run a callback immediately after completion or cancellation?
There is, you can use invokeOnCompletion
to achieve this.
Notification on Completion of a Coroutine
The Job
class has the method invokeOnCompletion
to attach a callback that will be invoked when the job completes, either successfully or with an exception.
In the example below we start a job and attach a callback with invokeOnCompletion
. The callback prints whether or not the job completed successfully.
fun main(): Unit = runBlocking {
val job = launch {
task(1)
}
job.invokeOnCompletion { throwable ->
if (throwable == null) {
println("Coroutine completed normally")
} else {
println("Coroutine failed: $throwable")
}
}
}
This prints:
Started task 1
Completed task 1
Coroutine completed normally
If instead the coroutine is cancelled the throwable passed to the callback will be a JobCancellationException
, so you’ll see something like:
Coroutine failed: kotlinx.coroutines.JobCancellationException:
StandaloneCoroutine was cancelled;
job=StandaloneCoroutine{Cancelled}@67205a84
If the coroutine errors the throwable will be the exception that caused the coroutine to fail.