Hero image

Managing multiple tasks with launch coroutine builder in Kotlin

Jan 08, 2024
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.