跪拜 Guibai
← All articles
Android

Coroutines Don't Replace Threads — They Replace CountDownLatch, AtomicInteger, and Callback Hell

By 潜龙勿用之化骨龙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Android teams still reach for Executors and hand-rolled synchronization out of habit, then pay for it in lifecycle bugs and unreadable state machines. Structured concurrency with async/awaitAll removes that entire class of defect — leaked tasks, missed completion callbacks, data races on shared collections — without changing execution speed.

Summary

A Moments posting flow — compress images, upload them, then submit — exposes exactly what coroutines replace. With a thread pool, a developer must hand-roll AtomicInteger completion counters, synchronized blocks for shared lists, and lifecycle shutdown logic to avoid leaks when an Activity dies mid-upload. The code is torn across callbacks and state checks. A coroutine version expresses the same flow top-to-bottom: compress, upload, submit. Under the hood, async/awaitAll replaces the counter-and-callback dance, and structured concurrency ties child coroutines to a parent scope so that cancelling a ViewModel cancels every in-flight task automatically. Coroutines don't make compression or network I/O faster; they strip out the JUC synchronization layer that thread pools demand, turning asynchronous orchestration back into sequential-looking code.

Takeaways
Thread pools solve "where does a task execute"; coroutines solve "how do multiple async tasks compose into a sequence."
A 10-image Moments flow with a thread pool requires AtomicInteger counters, synchronized blocks, and manual Executor shutdown to avoid leaks.
async/awaitAll replaces completion counters: map images to async blocks and call awaitAll() to resume only when every task finishes.
Structured concurrency means child coroutines are automatically cancelled when a parent scope (like viewModelScope) is cancelled, eliminating background-task leaks.
Coroutines do not reduce CPU or network time — an 800 ms compression still takes 800 ms — but they erase the synchronization code that thread pools force developers to write.
Conclusions

The framing shift from "lightweight thread" to "async flow organizer" matters because it changes what developers optimize: not thread count, but task composition and cancellation guarantees.

Many teams still conflate coroutines with performance; the real win is that structured concurrency makes lifecycle-safe async the default, not an afterthought bolted onto ExecutorService.shutdown().

async/awaitAll is a direct replacement for the CountDownLatch + AtomicInteger pattern that dominates Android thread-pool code, yet it reads as a single expression rather than scattered state checks.

Concepts & terms
Structured Concurrency
A concurrency model where tasks form a parent-child tree. Cancelling a parent (e.g., a ViewModel scope) automatically cancels all children, preventing orphaned background work and memory leaks.
async/awaitAll
Kotlin coroutine builders that launch concurrent subtasks and suspend until all complete. They replace manual completion counters (AtomicInteger, CountDownLatch) with a single expression that returns a list of results.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗