跪拜 Guibai
← Back to the summary

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:

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:

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:

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.

Screen_recording_20260802_081112.gif Source code: Understanding How Kotlin Coroutines Redefine Async Code Organization Through a Moments Posting Flow