Understanding Kotlin Coroutine Suspension and Resume Mechanism on Android
This article explains how Kotlin coroutines simplify asynchronous programming on Android by using suspension, detailing core concepts, APIs, dispatchers, suspend functions, and the underlying implementation through decompiled bytecode and continuation‑passing style, complemented by practical code examples and execution logs.
We know that Android offers many asynchronous solutions such as AsyncTask, Executor, RxJava, etc., which become cumbersome in complex business scenarios, leading to poor readability and potential bugs. Kotlin coroutines provide a simpler, higher‑performance alternative, largely because they implement coroutine suspension; understanding this suspension flow helps us use coroutines correctly and flexibly.
Coroutine Introduction
Coroutines originated from Simula and Modula‑2 languages; the term was introduced by Melvin Edward Conway in 1958 for building assembly programs.
A coroutine is a lightweight thread that improves development efficiency of asynchronous tasks in large projects. Kotlin coroutines are implemented differently on various platforms, and to use them well on Android we need to understand their design on this platform.
Since Kotlin 1.3, the coroutine library is included, enabling coroutine support on Android.
Android official documentation defines coroutines as:
Coroutines are a concurrent design pattern that you can apply on the Android platform to simplify asynchronous execution code.
Example comparing three sequential network requests implemented with ordinary Kotlin asynchronous code versus coroutine‑based code:
fun test1() { // Kotlin ordinary async implementation
Thread() { // switch to child thread
request1(param) { value1 -> // first async request returns
request2(value1) { value2 -> // second async request returns
request3(value2) { value3 -> // third async request returns
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(object : Runnable { // switch back to main thread
@Override
public void run() { // update UI
updateUI(value3)
}
})
}
}
}
}
}
fun test2() { // Kotlin coroutine async implementation
CoroutineScope(Dispatchers.IO).launch { // switch to child thread
val value1 = async { request1(param) }.await() // first async request returns
val value2 = async { request2(value1) }.await() // second async request returns
val value3 = async { request3(value2) }.await() // third async request returns
launch(Dispatchers.Main) { // switch back to main thread
updateUI(value3) // update UI
}
}
}test1 is the ordinary async version, test2 is the coroutine version; both achieve the same functionality, but the coroutine version is far more concise, improving readability and maintainability. Coroutines thus aim to simplify asynchronous code by eliminating traditional callbacks and allowing a sequential‑style code flow.
Core Concepts and APIs
Scope
A coroutine scope defines the code block where the coroutine runs. In source code, CoroutineScope specifies the scope, and several other scope interfaces extend it for different scenarios.
Scope
Meaning
Explanation
CoroutineScope
Create a local coroutine scope
Local scope can specify a Dispatcher and cancel all jobs via the returned Job object.
GlobalScope
Create a global coroutine scope
Creates a top‑level coroutine without a parent; it has no Job, so you must manage its child coroutines manually.
MainScope
Create a local scope with Dispatcher.Main
Combines CoroutineScope with Dispatcher.Main, allowing UI‑thread execution and Job management.
runBlocking
Create a local scope that blocks the current thread until completion
Blocks the calling thread while the coroutine runs; no Job is returned for management.
Creating and Launching Coroutines/Sub‑coroutines
Within a coroutine you can create new coroutines, forming parent‑child relationships that enable coordinated cancellation and exception handling. Creation APIs are extensions of CoroutineScope, so coroutines must be created inside a scope. Creation also starts the coroutine immediately.
Creation API
Meaning
Explanation
launch
Create a Job coroutine or sub‑coroutine
Creates a new coroutine and returns a Job for management.
async
Create a Deferred coroutine or sub‑coroutine
Deferred can be awaited for a result, unlike launch.
Dispatchers
Dispatchers determine which thread or thread pool executes a coroutine. Kotlin provides four default dispatchers under the Dispatchers class:
Dispatcher
Meaning
Explanation
Dispatchers.Main
Dispatch tasks to the Android main thread
Uses the main Looper’s handler to post tasks to the UI thread.
Dispatchers.Default
Dispatch tasks to the default worker pool
Executor thread pool sized based on CPU cores, suitable for CPU‑intensive work.
Dispatchers.IO
Dispatch tasks to an I/O‑optimized pool
Shares the pool with Default but allows more concurrent I/O tasks.
Dispatchers.Unconfined
Dispatch based on the current coroutine context
No specific thread; execution continues in the calling context.
Suspend Functions
A suspend function pauses coroutine execution without blocking the underlying thread. All suspend functions are marked with the suspend keyword and must be called from within a coroutine or another suspend function.
Suspend Function
Meaning
Explanation
join
Suspend the current coroutine until a child coroutine completes
Uses the Job’s
joinmethod to pause until the child finishes.
await
Suspend the current coroutine until a child coroutine returns a result
Part of the Deferred interface; returns the result after completion.
delay
Suspend the current coroutine for a specified time
Pauses execution for the given duration without blocking the thread.
withContext()
Suspend the outer coroutine until the block finishes on the specified context
Runs a code block on a given dispatcher and suspends until it returns, similar to
async/await.
Detailed Coroutine Suspension Process
From the previous sections we know the core concepts and APIs; coroutines achieve asynchronous behavior by suspending at suspend functions while not blocking the thread.
Below is a concrete example that demonstrates the suspension flow and log output order:
fun testInMain() {
Log.d("["+Thread.currentThread().name+"]testInMain start")
var job = CoroutineScope(Dispatchers.Main).launch { // start job coroutine
Log.d("["+Thread.currentThread().name+"]job start")
var job1 = async(Dispatchers.IO) { // start job1
Log.d("["+Thread.currentThread().name+"]job1 start")
delay(3000) // suspend job1 for 3 seconds
Log.d("["+Thread.currentThread().name+"]job1 end")
"job1-Return"
}
var job2 = async(Dispatchers.Default) {
Log.d("["+Thread.currentThread().name+"]job2 start")
delay(1000) // suspend job2 for 1 second
Log.d("["+Thread.currentThread().name+"]job2 end")
"job2-Return"
}
Log.d("["+Thread.currentThread().name+"]before job1 return")
Log.d("["+Thread.currentThread().name+"]job1 result = " + job1.await()) // suspend until job1 returns
Log.d("["+Thread.currentThread().name+"]before job2 return")
Log.d("["+Thread.currentThread().name+"]job2 result = " + job2.await()) // may resume immediately if result ready
Log.d("["+Thread.currentThread().name+"]job end")
}
Log.d("["+Thread.currentThread().name+"]testInMain end")
}Sample log output (chronological order) highlights the interleaving of main‑thread logs, job coroutine logs, and the delayed resumption of job1 and job2:
10:15:04.046 26079-26079/com.example.myapplication D/TC: [main]testInMain start
10:15:04.067 26079-26079/com.example.myapplication D/TC: [main]testInMain end
10:15:04.080 26079-26079/com.example.myapplication D/TC: [main]job start
10:15:04.083 26079-26079/com.example.myapplication D/TC: [main]before job1 return
10:15:04.086 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job1 start
10:15:04.087 26079-26684/com.example.myapplication D/TC: [DefaultDispatcher-worker-2]job2 start
10:15:05.090 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job2 end
10:15:05.095 26079-26079/com.example.myapplication D/TC: [main]button-2 onclick now
10:15:07.090 26079-26685/com.example.myapplication D/TC: [DefaultDispatcher-worker-3]job1 end
10:15:07.091 26079-26079/com.example.myapplication D/TC: [main]job1 result = job1-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC: [main]before job2 return
10:15:07.091 26079-26079/com.example.myapplication D/TC: [main]job2 result = job2-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC: [main]job endKey observations from the log:
Step 1: Main thread calls testInMain and logs "[main]testInMain start".
Step 2: testInMain finishes quickly, logging "[main]testInMain end".
Step 3: The job coroutine is scheduled on the main thread and logs "[main]job start".
Step 4: job logs "before job1 return" before awaiting job1 .
Step 5: job suspends at job1.await() , releasing the main thread.
Step 6: job1 runs on an IO worker thread, logs start, then suspends for 3 seconds via delay .
Step 7: job2 runs on a Default worker thread, logs start, then suspends for 1 second.
Step 8: After 1 second, job2 resumes, logs end, and its result becomes available.
Step 9: Meanwhile the UI thread processes a button click, demonstrating that the main thread is not blocked.
Step 10: After 3 seconds, job1 resumes, logs end, and job receives its result.
Step 11‑14: job continues, logs results, and finally ends.
The entire testInMain execution completes about 3.5 seconds after it started.
From the diagram we can draw several conclusions:
The job coroutine’s execution is paused by await , but the underlying thread continues to run other tasks.
job1 and job2 run in parallel on separate worker threads; they do not wait for each other.
While job is awaiting results, the main thread remains free to handle UI events.
When job1 is delayed, its thread is not blocked; job2 may resume on the same worker thread.
Resumption does not guarantee execution on the original thread, as shown by the logs.
Thus, coroutine suspension does not block the thread; it merely pauses the coroutine’s continuation until the suspend function completes, after which the coroutine resumes.
Implementation Principles of Coroutine Suspension
To understand Kotlin’s coroutine implementation on Android we need to inspect the decompiled bytecode. Because the compiler rewrites suspend functions, we use a decompiler (e.g., Bytecode Decompile) to view the generated Java.
The core decompiled code for the previous example looks like this:
// TestCoroutine.decompiled.java
public final void testInMain() {
Log.d("cjf---", var10001.append("testInMain start").toString());
Job job = BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope(Dispatchers.getMain()), null, null, (Function2)(new Function2((Continuation)null) {
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
// split into label blocks (see below)
}
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { /* ... */ }
public final Object invoke(Object var1, Object var2) { /* ... */ }
}), 3, null);
Log.d("cjf---", var10001.append("testInMain end ").toString());
}
// invokeSuspend of the job coroutine (simplified)
public final Object invokeSuspend(@NotNull Object $result) {
Object COROUTINE_SUSPENDED = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
Log.d(var10001.append("job start").toString());
Deferred job1 = BuildersKt.async$default(...);
Deferred job2 = BuildersKt.async$default(...);
Log.d(var10001.append("before job1 return").toString());
this.L$0 = job2; this.L$1 = var5; this.L$2 = var6; this.label = 1;
Object result = job1.await(this);
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
break;
case 1:
// resume after job1.await
...
this.label = 2;
result = job2.await(this);
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
break;
case 2:
// resume after job2.await
...
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Log.d(...);
return Unit.INSTANCE;
}The decompiled version shows that the coroutine body is transformed into a state machine using a label field and a switch statement. Each suspend point becomes a separate case block.
When a suspend function like await returns COROUTINE_SUSPENDED , invokeSuspend returns immediately, leaving the coroutine in a suspended state. The continuation object (a Continuation implementation) is stored so that the coroutine can be resumed later.
The Continuation interface provides resumeWith , which is called when the awaited result becomes available. The core loop in BaseContinuationImpl.resumeWith repeatedly invokes invokeSuspend until a non‑suspended result is produced, propagating results up the continuation chain.
// BaseContinuationImpl.resumeWith (simplified)
while (true) {
probeCoroutineResumed(current);
with(current) {
val completion = completion!!
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (e: Throwable) {
Result.failure(e)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}The awaitSuspend implementation in JobSupport.kt creates an AwaitContinuation , registers it as a completion handler on the awaited job, and then calls getResult() . If the result is not ready, it returns COROUTINE_SUSPENDED , causing the caller coroutine to pause.
private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
val cont = AwaitContinuation(uCont.intercepted(), this)
cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(this, cont).asHandler))
cont.getResult()
}When the awaited job completes, its completion handler invokes cont.resumeWith(result) , which triggers the stored continuation to resume the original coroutine at the next label.
Putting everything together, the suspension‑and‑resume flow works as follows:
The outer job coroutine starts on the main dispatcher.
job1 and job2 are launched on IO and Default dispatchers, respectively, and run in parallel.
job calls job1.await() , which registers a continuation and returns COROUTINE_SUSPENDED , pausing job without blocking the main thread.
After the delay, job1 completes, its handler resumes job via resumeWith , which continues execution at the next label.
The same process repeats for job2.await() .
Summary
This article used log output and decompiled source code to explain Kotlin coroutine suspension and resume processes. The key points are:
Suspension and resumption are orchestrated by invokeSuspend and resumeWith , forming a continuation chain.
Coroutine, SuspendLambda, and Dispatcher classes all implement Continuation , enabling hierarchical resumption.
The suspension flow is achieved through continuation encapsulation and CPS (Continuation‑Passing‑Style) transformation, allowing asynchronous code to appear sequential.
Understanding these mechanisms lets developers leverage coroutines for clean, safe, and maintainable asynchronous task scheduling in Android applications. For new projects, you can wrap existing async APIs with suspend functions, and use Jetpack scopes such as viewModelScope or lifecycleScope to manage coroutine lifecycles.
Many Jetpack libraries provide comprehensive coroutine support extensions. Some libraries also expose their own coroutine scopes for structured concurrency. ViewModel includes a set of KTX extensions that work directly with coroutines; these extensions are part of the lifecycle-viewmodel-ktx library.
Congratulations to “Wolf Likes Summer”, “_”, “A Ce~”, “Orange Heart”, “Run Run”! Please add the editor’s WeChat (sohu-tech20) to receive a book.
PS: If you cannot leave a comment, go to WeChat Home → Pull down → Long press to delete the mini‑program “Small Interaction”, then re‑enter the comment board.
Also check out these related articles:
Kotlin Guide (Deep Dive)
Kotlin Design Patterns
JetPack Compose from Exploration to Practice
Flutter Android Flavor Packaging Practice
Understanding Android 11 Scoped Storage
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.