Kotlin Coroutines Async Await Sequence

val one = async { one() }
val two = async { two() }
val int1 = one.await()
val int2 = two.await()

What this does:

  1. spawn task one
  2. spawn task two
  3. await on task one
  4. await on task two

val one = async { one() }.await()
val two = async { two() }.await()

What this does:

  1. spawn task one
  2. await on task one
  3. spawn task two
  4. await on task two

There's no concurrency here, it's purely sequential code. In fact, for sequential execution you shouldn't even use async. The proper idiom is

val one = withContext(Dispatchers.Default) { one() }
val two = withContext(Dispatchers.Default) { two() }

The thumb-rules:

  • Use withContext when you do not need the parallel execution.
  • Use async only when you need the parallel execution. Both withContext and async can be used to get the result which is not possible with the launch.
  • Use withContext to return the result of a single task.'
  • Use async for results from multiple tasks that run in parallel.

Check this for more details


In the first variant you get a Deferred<Int> for both async-calls. As the documentation of Deferred shows so nicely there are several states the deferred object might be in. From the outside that state is now either new or active but surely not completed yet. On your second variant however the first async-await needs a completed state already otherwise you could not have any value there. However on your async{one()}.await() the second async is not known yet. Note also that the return value of await() is now Int and not Deferred anymore, so the coroutine must have been executed by then. Check also the documentation of await().

In other words:

val one = async { one() }
val two = async { two() }

Both one and two are now Deferred<Int>. None has been called yet (or might have been called yet). As soon as you call one.await() it may start already both one and two, just because it has the resources for it (even if you didn't use two.await() anywhere in your code).

On the second variant however:

val one = async { one() }.await()
val two = async { two() }.await()

Even though it creates a coroutine for async {one()} it must set a value to one immediately, because you are calling await() on it. The types of one and two are both Int. So as soon as the first of those lines is hit, the async code needs to be executed immediately. By then nobody knows that another asynchronous call has to be executed as we wait for the value of the first. If the first wouldn't have an await, the coroutine would again be executed in parallel, e.g.:

val one = async { one() }
val two = async { two() }.await()

will execute one() and two() in parallel.

So maybe this can be summarized to: only those coroutines can be executed in parallel on an await, that are known/spawned by then.