Awaiting Kotlin suspend functions in serial, parallel and lazy startup modes

Kotlin allows a thread of execution to be suspended until some other asynchronous function completes and returns a result. Suspend functions are most commonly used when a response from an external resource is needed to complete the suspend function.

Kotlin allows a thread of execution to be suspended until some other asynchronous function completes and returns a result. Suspend functions are most commonly used when a response from an external resource is needed to complete the suspend function. Most often the external resource is a web service API or a connected network or Bluetooth device.

Creating a Suspend Function

In Kotlin a suspend function is declared with the suspend keyword:

suspend fun doSomethingUsefulOne(): Int {
  delay(100L) // pretend we are doing something useful here
  return 13
}

suspend fun doSomethingUsefulTwo(): Int {
  delay(100L) // pretend we are doing something useful here, too
  return 29
}

For simplicity, these two functions use the delay function to pause their thread for some number of milliseconds before continuing.  In a production application this is where an external, blocking network request could be made.

Calling a Suspend Function in Serial

The most straightforward call to a suspend function is a simple call, which will suspend the calling thread until the suspend function returns. In this example, one function will run at a time, e.g. doSomethingUsefulTwo will not run until doSomethingUsefulOne has returned.

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun doSomethingUsefulOne(): Int {
  delay(100L) // pretend we are doing something useful here
  return 13
}

suspend fun doSomethingUsefulTwo(): Int {
  delay(100L) // pretend we are doing something useful here, too
  return 29
}

fun main() {
  GlobalScope.launch {
    val time = measureTimeMillis {
      val one = doSomethingUsefulOne()
	    val two = doSomethingUsefulTwo()
      println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
  }
}

/*
	The answer is 42 Completed in 307 ms
*/

Functions called in parallel (with await to synchronize)

What if we don't want to wait for the doSomethingUsefulOne to return before running doSomethingUsefulTwo, but also we don't want the calling thread to continue until both functions have run?  For this case, use the async keyword before the function wrapped in a code block, and call each code block with the .await() function.

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun doSomethingUsefulOne(): Int {
    delay(200L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(100L) // pretend we are doing something useful here, too
    return 29
}

fun main() {
    GlobalScope.launch {
        val time = measureTimeMillis {
          val one = async { doSomethingUsefulOne() }
          val two = async { doSomethingUsefulTwo() }
          println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
    }
}

/*
The answer is 42 Completed in 236 ms (71ms faster than serial run)
*/

This approach is similar to the Serial approach, except it allows both suspend functions to run a the same time on different threads. The execution is reduced from 307ms to 236ms.

Lazy Start

Building on the Parallel approach, it's possible to create the async code blocks, but control when each async code block starts explicitly.  This could be helpful to allow doSomethingUsefulOne to get started while doing other work to prepare for the call of doSomethingUsefulTwo.

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun doSomethingUsefulOne(): Int {
    delay(200L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(100L) // pretend we are doing something useful here, too
    return 29
}

fun main() {
    GlobalScope.launch {
        val time = measureTimeMillis {
          val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
          val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
          
          // some computation
          one.start() // start the first function
          delay(100L) // This delay has little impact on total time since doSomethingUsefulOne is the bottleneck
          two.start() // start the second function
          
          println("The answer is ${one.await() + two.await()}")   
        }
        println("Completed in $time ms")
    }
}

/*
The answer is 42 Completed in 243 ms
*/