skip to content

How do you run two independent suspending calls concurrently and combine their results, and how is that different from calling them one after another?

level: middleimportance: must knowfreq 75%

answer

  1. start all async, then await all
  2. max(t) not sum(t)
  3. inline .await() = accidental sequential
  4. host in coroutineScope
  5. independent tasks only

basics

~10 s

Start each call with async so they run at the same time, then await both. Calling them sequentially (just suspend calls) waits for the first to finish before starting the second, so it's slower.

solid answer

~40 s

Wrap each independent task in async so both coroutines start eagerly and run concurrently, then await each Deferred and combine. This is parallel decomposition: total time is roughly max(t1, t2) instead of t1 + t2. If you instead write two plain suspend calls in sequence, the second only starts after the first returns, giving sequential timing. Crucial detail: call async for both *before* awaiting either. A common bug is `async { a() }.await() + async { b() }.await()` — awaiting the first inline makes it sequential again because b()'s async hasn't been created when a().await() suspends. Do the work inside coroutineScope so the calls share a scope and failures propagate correctly.

code

kotlin · 5 lines
kotlin
suspend fun dashboard(): Dashboard = coroutineScope {
    val stats = async { loadStats() }
    val feed  = async { loadFeed() }
    Dashboard(stats.await(), feed.await()) // concurrent
}

go deeper

for a junior

Recognizes that two async calls then two awaits run concurrently versus sequential suspend calls.

for a middle

Articulates max vs sum timing and avoids the inline-await ordering bug; uses coroutineScope.

for a senior

Explains overlap on a single dispatcher via suspension and ties it to structured-concurrency failure semantics.

for a principal

Weighs concurrency limits, backpressure, and resource contention when fanning out many parallel tasks.

## The goal: parallel decomposition **Parallel decomposition** means splitting work into independent pieces, running them concurrently, then combining results. When pieces don't depend on each other, running them in parallel cuts wall-clock time from the **sum** of their durations to roughly the **maximum**. ## Sequential (slow) version ```kotlin suspend fun loadSequential(): Combined = coroutineScope { val a = fetchA() // suspends until done val b = fetchB() // only starts AFTER a finishes Combined(a, b) // total ~ tA + tB } ``` Plain suspend calls run one at a time in program order. ## Concurrent (fast) version ```kotlin suspend fun loadConcurrent(): Combined = coroutineScope { val da = async { fetchA() } // starts now val db = async { fetchB() } // starts now, concurrently Combined(da.await(), db.await()) // total ~ max(tA, tB) } ``` Both `async` calls launch their coroutines eagerly, so both are in flight before either `await()` runs. ## The classic ordering bug ```kotlin // WRONG — accidentally sequential val result = async { fetchA() }.await() + async { fetchB() }.await() ``` Here `async { fetchA() }.await()` runs to completion before the second `async` is even created, so there is no overlap. **Create all the Deferreds first, then await.** ## Why `coroutineScope` Running the `async` calls inside `coroutineScope { }` gives them a parent scope. This enforces **structured concurrency**: the function won't return until both children finish, and if one fails the other is cancelled. Calling `async` on `GlobalScope` would break that. ## Key APIs/keywords - `async { }` started for each independent task - `await()` once per Deferred, after all are started - `coroutineScope { }` to host them with structured concurrency

  • Why does `async { x() }.await()` followed by `async { y() }.await()` not run in parallel?
    The first await() completes before the second async is created, so y() never overlaps x(). Create both Deferreds first.
  • If fetchA and fetchB both hit the same coroutine on a single-threaded dispatcher, do they still overlap?
    Yes for I/O-bound suspension: while one is suspended awaiting I/O the dispatcher runs the other. They overlap in time even on one thread.

Like putting two pots on the stove at once vs. cooking one then the other — start both burners before checking either.

saying these in an interview costs you the question

  • Awaiting each Deferred inline so the code is secretly sequential
  • Claiming concurrency requires multiple threads (suspension interleaves on one)
  • Using GlobalScope.async instead of a structured scope
  • Believing two sequential suspend calls already run concurrently

context