Are `suspend` function calls concurrent or sequential by default? Show how to actually run two suspend calls in parallel.
answer
- Suspend calls are sequential by default
- Suspension != concurrency
- async returns Deferred; await collects the result
- Wrap fan-out in coroutineScope for structured concurrency
- Lazy async + sequential await re-serializes
basics
~10 sBy default suspend calls run one after another (sequentially). To run them in parallel you start each in its own coroutine, e.g. with async, then await both.
solid answer
~40 sInside a coroutine, calling suspend functions one after another is **sequential**: the second call doesn't start until the first returns, even though neither blocks a thread. Suspension is not concurrency. To run independent suspend calls concurrently you launch separate coroutines — typically `async { }` for each, returning `Deferred<T>`, then call `.await()` on both. Because the `async` builders start their coroutines eagerly (with the default `CoroutineStart.DEFAULT`), the two operations overlap; awaiting just collects results. Use `coroutineScope { }` to host them so structured concurrency cancels both if one fails. For fire-and-forget you'd use `launch`. A common bug is writing `val a = fetchA(); val b = fetchB()` expecting parallelism — it's sequential; the fix is `val a = async { fetchA() }; val b = async { fetchB() }; a.await() + b.await()`.
code
kotlin · 10 linesimport kotlinx.coroutines.*
suspend fun fetchA(): Int { delay(1000); return 1 }
suspend fun fetchB(): Int { delay(1000); return 2 }
suspend fun parallel(): Int = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
a.await() + b.await() // ~1000ms total, not 2000ms
}go deeper
Knows you need async/await (not just suspend) to run things in parallel.
Writes correct coroutineScope + async/await fan-out and explains sequential default.
Discusses structured concurrency, exception propagation, lazy-start pitfalls, and launch vs async.
Designs concurrency patterns/utilities and reasons about cancellation and failure semantics across a service.
## Default behavior: sequential A `suspend` function call is a normal, blocking-looking call from the coroutine's point of view: control doesn't move past it until it completes. So this is **sequential**, taking the sum of both durations: ```kotlin suspend fun loadAll() { val a = fetchA() // wait for A to finish val b = fetchB() // only then start B } ``` Suspension means neither call blocks a *thread*, but they still execute **in order** within this one coroutine. Suspension is not the same as concurrency. ## Running in parallel with `async` To overlap independent work, start each in its own coroutine with `async`, which returns a `Deferred<T>`: ```kotlin suspend fun loadAll() = coroutineScope { val aDeferred = async { fetchA() } // starts immediately val bDeferred = async { fetchB() } // also starts immediately val a = aDeferred.await() // suspend until A done val b = bDeferred.await() // B likely already done a to b // total ~= max(A, B) } ``` - `async` launches the block eagerly (default `CoroutineStart.DEFAULT`), so both operations are in flight before either `await`. - `.await()` suspends until that `Deferred` completes and returns its result (or rethrows its exception). ## Why `coroutineScope`? Hosting the `async` calls in `coroutineScope { }` gives **structured concurrency**: if either child fails, the scope cancels the other and propagates the exception; the function doesn't return until both finish. Using `GlobalScope.async` instead would leak coroutines and lose cancellation — an anti-pattern. ## `launch` vs `async` - `launch` returns a `Job` and is for fire-and-forget side effects (no result). - `async` returns a `Deferred<T>` and is for producing a value you'll `await`. ## Lazy start `async(start = CoroutineStart.LAZY) { ... }` defers execution until the first `await()` or `start()`. With lazy starts, calling `await()` sequentially would serialize them again — a subtle gotcha. ## Key takeaway Suspend = non-blocking, but **still sequential** unless you explicitly fan out into multiple coroutines.
- Why does writing `val a = fetchA(); val b = fetchB()` take the sum of both durations?Because each suspend call completes before the next begins within the same coroutine — they run sequentially. You must launch separate coroutines (async) to overlap them.
- What happens to the other async if one of them throws?Inside coroutineScope, the failing child cancels the scope, which cancels the sibling async, and the exception propagates out of coroutineScope.
Reading two letters yourself one after another (sequential suspend) versus handing each to a friend to read simultaneously and then collecting both summaries (async/await).
saying these in an interview costs you the question
- Believing two sequential suspend calls run in parallel
- Equating suspension with concurrency
- Using GlobalScope.async for fan-out (leaks, no cancellation)
- Not awaiting async results (orphaned work / swallowed exceptions)
- Lazy async with sequential await and expecting parallelism