Kotlin / Concurrency, interoperability, and integrated design / Lesson 13
Own concurrent work with structured coroutines
Distinguish suspension from blocking and use scopes, launch, async, await, and dispatcher assumptions deliberately.
On this page
Lesson goals
- Explain why suspend alone neither launches work nor selects a background thread.
- Keep child work inside a caller-owned lifetime and combine its results.
Suspension is a language capability, not a thread choice
A suspending function can suspend and later resume within coroutine machinery. The suspend modifier does not itself start a coroutine, make code parallel, or move blocking work to a background thread. Coroutines execute on threads according to their context and dispatcher. Most high-level builders and utilities come from kotlinx.coroutines, a library separate from the core language and standard library. Examples in this lesson require a compatible project dependency already configured by the learner. This authoring environment has no Kotlin compiler or coroutine dependency, so every program remains illustrative and unexecuted.
A scope gives child work an accountable lifetime
coroutineScope creates a lexical scope that does not finish until its block and children finish. If an ordinary child failure makes the scope fail, sibling work is cancelled and the failure is rethrown to the caller according to the scope contract. This fits one task whose parts are all needed. Avoid launching work in an unrelated global lifetime merely to get an immediate return; the caller then loses straightforward cancellation and failure ownership. Long-lived application work still needs a deliberately owned scope that is cancelled during its lifecycle, rather than a scope created anonymously for each request.
Use async for a result that the task will await
async returns a Deferred whose await retrieves a result or reports failure. The two async blocks below are children of the same scope, and the final sum is printed only after both results are available. They contain trivial fixed computations, so this demonstrates structure rather than useful parallel speedup. Deferred values should not become abandoned error containers. Under ordinary structured scopes, child failure can cancel the scope even before a later await expression; catching only await in the wrong place is not a universal strategy for isolating failures.
import kotlinx.coroutines.*
suspend fun total(): Int = coroutineScope {
val first = async { 2 }
val second = async { 3 }
first.await() + second.await()
}
suspend fun main() {
println(total())
}
5
Expected output is 5. Compatible kotlinx.coroutines is required. No timing, thread count, compiler execution, or performance improvement is claimed.
Use launch for completion rather than a returned payload
launch returns a Job that represents lifecycle and completion rather than a business result. join waits for its completion but does not turn it into a Deferred result. The example uses runBlocking as a small JVM-style synchronous entry bridge; it blocks its calling thread while the coroutine work completes. It is not a recommendation to wrap UI handlers or every suspending function in runBlocking. Within coroutine code, call suspending functions directly or use the appropriate structured builder instead of repeatedly introducing blocking bridges.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { println("child complete") }
job.join()
println("joined")
}
child complete
joined
Expected lines are child complete then joined because join completes after the child. This program needs kotlinx.coroutines and was not compiled or run here.
Choose dispatchers for the actual work and platform
A CPU-bound calculation and a blocking library call place different demands on threads. A dispatcher choice must fit the platform, library behavior, and application lifecycle; Dispatchers.Main also requires an appropriate platform integration. Switching context does not make an unsafe shared object thread-safe, and coroutineScope alone does not switch to a background dispatcher. Prefer libraries with genuine suspending APIs where available. Document which operations block, how cancellation is observed, and which owner limits concurrency before claiming that the architecture remains responsive under load.
Common errors and fixes
A suspend function freezes the calling UI thread.
- Why it happens
- Blocking work remained on the caller context.
- How to fix it
- Use an appropriate suspending API or explicitly owned blocking-work boundary.
- Prevent it next time
- Document blocking behavior instead of inferring it from suspend.
Work continues after its screen or request ends.
- Why it happens
- The coroutine was launched in an unrelated lifetime.
- How to fix it
- Attach it to a scope owned and cancelled by the relevant lifecycle.
- Prevent it next time
- Review the owner of every created scope.
A Job is treated as if it contains the async result.
- Why it happens
- Completion and value-producing builders were confused.
- How to fix it
- Use async and await for a value, launch and join for completion.
- Prevent it next time
- Read the builderโs return type before composing tasks.
Check your understanding
1. Does calling two suspend functions one after another create concurrency?
Show worked solution
No. Sequential calls preserve sequential order unless the implementation starts concurrent work. Use a structured scope with deliberate child builders when both operations may overlap, and await their results. First verify that concurrent execution preserves business ordering and resource limits; concurrency is a choice with semantic costs.
2. Why is runBlocking inappropriate as a general fix inside a suspending function?
Show worked solution
It introduces blocking into code that already has a coroutine context and can waste or deadlock constrained execution resources depending on the surrounding design. Call suspending operations directly and use coroutineScope or withContext for the required structure or context. A synchronous entry bridge is a specific boundary, not a universal coroutine wrapper.
Key takeaways
- Coroutine structure gives lifetime and failure ownership to concurrent work.
- Suspension, blocking, concurrency, and parallelism are distinct concepts.