Coroutines Don't Replace Threads — They Replace CountDownLatch, AtomicInteger, and Callback Hell
Many Android developers, when first encountering Kotlin coroutines, have this perception:
Are coroutines just a lighter-weight thread?
So many articles emphasize:
- Coroutines are lighter than threads;
- One thread can run many coroutines;
- Coroutine switching costs are low.
I previously wrote an article:
Why does the Kotlin official website no longer emphasize that "coroutines are lightweight threads"?
If you haven't read it, you can check it out first.
Let's look at a Moments posting scenario:
A Typical Business Case: Posting to Moments
Assume the user selects multiple images:
Flow:
Select Images
↓
Compress Images Concurrently
↓
Upload to Server Concurrently
↓
Submit Moments Data
↓
Post Successful
This is a very typical asynchronous task chain.
If using a traditional thread pool:
You need to manage:
- Task submission;
- Task completion counting;
- Phase switching;
- Thread safety;
- Lifecycle;
- Exception handling.
If using coroutines:
The business flow can be expressed directly.
1. Traditional ThreadPool
Compress 10 images
↓
All completed
↓
Upload 10 images
↓
All completed
↓
Submit Moments
The problem emerges.
[UI Thread] postMoment(uris)
│
├─> Executor.execute (dispatch 9 compression tasks)
│ │
│ ├─ [Thread A] compress image1 -> synchronized { add to List } -> count++ -> count==9? (No)
│ ├─ [Thread B] compress image2 -> synchronized { add to List } -> count++ -> count==9? (No)
│ └─ [Thread N] compress image9 -> synchronized { add to List } -> count++ -> count==9? (Yes! trigger callback)
│ │
│ ┌────────────────────────────────── onCompressionFinished() <────┘
│ │
│ ├─> Executor.execute (dispatch N upload tasks)
│ │ ├─ [Thread X] upload image1 -> synchronized { add to URL } -> count++ -> count==all? (No)
│ │ └─ [Thread Z] upload imageN -> synchronized { add to URL } -> count++ -> count==all? (Yes! trigger)
│ │ │
│ └─> ┌────────────────────────────── onUploadFinished() <───────────────┘
│ │
│ └─> Executor.execute { repository.finalizePostSync() }
│
[UI Thread] Update success UI state (requires Handler or Flow.update)
The code expression is not linear, but torn apart.
1. Need to record task completion count yourself
For example:
val count = AtomicInteger(0)
val current = count.incrementAndGet()
if(current == total){
startUpload()
}
Because the thread doesn't know:
When all tasks are completed.
So the developer must use JUC synchronization (AtomicInteger can be avoided here, but JUC synchronization is definitely required).
2. Need to handle shared data
For example:
Multiple threads simultaneously add compression results:
val images = mutableListOf<ByteArray>()
Must:
synchronized(images){
images.add(data)
}
Otherwise:
May happen:
Thread1 add
Thread2 add
Data race
3. Complex lifecycle management
The biggest problem on Android:
The page exits.
But the thread is still running:
Activity destroy
↓
Thread continues uploading
↓
Memory leak
So you need:
executor.shutdown()
Even:
shutdownNow()
Combined with:
interrupt check
2. Coroutine: Rewriting Async Back into Synchronous Code
Coroutine version:
[UI Thread] postMoment(uris)
│
├─> launch (start coroutine)
│ │
│ ├─> performCompression (concurrent compression) ──┐
│ │ ├─ async (image1) -> suspend and wait │
│ │ ├─ async (image2) -> suspend and wait ├─> Dispatchers.Default (concurrent execution)
│ │ └─ awaitAll() <─── all completed callback ────┘
│ │
│ ├─> performUpload (concurrent upload) ──────┐
│ │ ├─ async (image1) -> suspend and wait │
│ │ ├─ async (image2) -> suspend and wait ├─> Dispatchers.IO (concurrent execution)
│ │ └─ awaitAll() <─── all completed summary ──┘
│ │
│ └─> finalizePost (final post) ───────> Dispatchers.IO (single execution)
│
[UI Thread] Update success UI state
Code reading method:
Compress
↓
Upload
↓
Submit
Completely consistent with the business flow.
But internally:
It is still asynchronous execution.
3. async + awaitAll: Replacing Counter Waiting
Before:
Waiting for 10 images:
Needed:
AtomicInteger
+
if(count==total)
Now:
val result =
images.map {
async {
compress(it)
}
}.awaitAll()
Meaning:
Create multiple tasks:
Task1
Task2
Task3
Task4
Then:
awaitAll()
Wait for all to complete
The developer doesn't need to know:
Which thread completed.
Nor maintain:
Completion count.
4. Structured Concurrency: The Coroutine's Biggest Killer Feature
The most important concept of coroutines:
Is not lightweight.
But:
Structured Concurrency.
What is structured concurrency?
Tasks have parent-child relationships.
For example:
ViewModel
|
launch
|
+--Compression task 1
+--Compression task 2
+--Upload task 1
+--Upload task 2
When ViewModel is destroyed:
viewModelScope.cancel()
Result:
Parent task cancelled
↓
All child tasks cancelled
No background tasks left behind.
Traditional threads:
ViewModel
|
ThreadPool
|
Thread
There is no natural relationship between threads.
Lifecycle:
Requires manual maintenance.
5. Coroutines Don't Make Tasks Execute Faster
Misunderstandings easily arise here.
For example:
Compress image:
800ms
Upload:
1200ms
Coroutines won't:
800ms → 400ms
Because:
The real time consumers are:
- CPU computation;
- Network IO.
What coroutines improve is:
Development efficiency and system controllability.
It reduces:
AtomicInteger
synchronized
Callback
Future
CountDownLatch
Thread lifecycle management
6. What Do Coroutines Truly Replace?
Many people say:
Coroutines replace threads.
Actually, that's not accurate.
Threads still exist.
What coroutines truly replace is:
The large number of complex mechanisms that were created in the past to organize asynchronous flows:
Callback
↓
Future
↓
CountDownLatch
↓
AtomicInteger
↓
synchronized
↓
State machine code
Finally
The problem thread pools solve:
Where does the task execute?
For example:
Which thread executes the upload?
The problem coroutines solve:
How to combine multiple asynchronous tasks?
For example:
Upload after compression completes
Post after upload completes
Auto-cancel when page exits
So:
Threads are execution resources, coroutines are an organizational model for asynchronous flows.
The place where Kotlin coroutines truly change Android development is not making threads disappear, but freeing developers from JUC synchronization state.
Before:
Callback
+
ThreadPool
+
Lock
+
Counter
+
Lifecycle management
Now:
suspend
+
async
+
await
+
Structured Concurrency
This is the architectural-level change brought by coroutines.
Source code:
Understanding How Kotlin Coroutines Redefine Async Code Organization Through a Moments Posting Flow