异步编程思想的演进:从 Rx 到 Kotlin Coroutines
从 Callback、Future 到 RxJava 与 Kotlin Coroutines——异步编程范式的演进脉络、设计哲学与选型依据
无论是 JavaScript 的 Promise / async-await,还是 Android / Kotlin 世界里的 Future、RxJava、Coroutines,本质上都在解决同一个问题:如何在多线程环境中,让异步逻辑可读、可组合、可测试,同时不阻塞 UI 线程。
作为开发者,理解异步机制的思想比熟悉具体 API 更重要——框架会迭代,但范式一旦掌握,迁移成本会显著降低。

上图从左至右呈现异步范式的四代演进。Callback 解决「有没有回调」的问题,Future 解决「如何拿到一次性结果」,Rx 解决「如何组合持续变化的数据流」,Coroutines 解决「如何用同步写法表达结构化并发」。下文逐一拆解。
一、核心问题:异步编程在解决什么
1.1 Android 的线程约束
Android 规定 Main Thread 负责 UI 渲染,网络请求、磁盘 IO、复杂计算必须在后台线程执行。若 IO 阻塞 Main Thread,会出现 ANR;若 UI 更新不在 Main Thread,会抛出 CalledFromWrongThreadException。
因此,任何 Android 异步方案都必须回答三个问题:
| 问题 | 说明 |
|---|---|
| 在哪里执行 | 任务跑在哪个线程(IO、Default、Main) |
| 如何传递结果 | 后台完成后如何把数据安全地回传 UI 层 |
| 如何组合多个异步 | 串行、并行、竞态、取消、错误传播 |
线程调度框架的引入背景,详见 为什么要引入线程调度框架。
1.2 演进脉络
Callback → Future → RxJava → Kotlin Coroutines (+ Flow) │ │ │ │ 嵌套地狱 一次性结果 响应式流 结构化并发
这不是简单的「后者取代前者」——许多代码库 today 仍并存 RxJava 与 Coroutines(如 Room 同时支持两者)。理解各范式的适用边界,比站队更重要。
二、Callback 与 Future:命令式异步的起点
2.1 Callback:回调地狱
最早的异步写法通过嵌套回调传递结果。当业务链变长(压缩 → 抠图 → 滤镜),回调层层嵌套,错误处理分散,代码难以维护。具体痛点可用 Kotlin Coroutine 一文中的回调示例 对照理解。
2.2 Future / CompletableFuture
Future 代表一次性的异步结果——任务提交后在某个时刻 get() 拿到值。CompletableFuture 在此基础上支持链式组合(thenApply、thenCompose)。
| 特征 | 说明 |
|---|---|
| 思维模型 | 命令式:「启动任务 → 等待/回调结果」 |
| 适用场景 | 单次异步操作、简单链式组合 |
| 局限 | 难以表达持续变化的数据流;组合复杂时 API 冗长;取消与生命周期绑定弱 |
Future 强调的是任务的生命周期,而非数据随时间流动的过程。
三、RxJava:响应式编程范式
3.1 核心思想
Rx(Reactive Extensions)将异步重新定义为对随时间变化的数据流(Stream)的处理。核心类型包括:
| 类型 | 语义 |
|---|---|
Observable | 0..N 个元素,无背压 |
Single | 恰好 1 个元素或错误 |
Completable | 仅完成信号,无数据 |
Flowable | 带背压的 Observable |
「用声明式的方式描述数据变化,而不是命令式地等待结果。」
订阅者通过 subscribe() 观察流的事件:onNext、onError、onComplete。操作符(map、flatMap、zip、merge)以函数式方式组合、变换、合并多条流。
3.2 线程调度
RxJava 2/3 通过 Scheduler 显式控制执行与观察线程:
service.fetchData()
.subscribeOn(Schedulers.io()) // 订阅时在 IO 线程执行
.observeOn(AndroidSchedulers.mainThread()) // 结果回调到 Main Thread
.subscribe({ data -> updateUI(data) }, { error -> showError(error) })subscribeOn 决定上游执行线程,observeOn 决定下游观察线程——这是 Rx 面试与实战的高频考点。
3.3 串行与并行
// 串行:A 完成后根据结果调用 B
fun serial(): Single<String> =
service.fetchLength()
.flatMap { length -> service.fetchList(length) }
.map { it.joinToString(", ") }
// 并行:A、B 同时执行,结果合并
fun parallel(): Single<String> =
Single.zip(service.fetchA(), service.fetchB()) { a, b -> a + b }更多实战对比见 android-async-util。
3.4 优势与代价
优势
- 操作符生态成熟,复杂流组合表达力强
- 天然适合「持续推送」场景(传感器、WebSocket、搜索联想)
- 错误作为流事件传播,链路清晰
代价
- 学习曲线陡峭,操作符组合/debug 门槛高
- 需手动管理
Disposable与生命周期,否则内存泄漏 - 冷流/热流、背压等概念增加认知负担
- 调试栈 trace 不如同步代码直观
四、Kotlin Coroutines:结构化并发
4.1 核心思想
Coroutines 不是单纯的 Future,也不是 Rx 的翻版。它在设计上融合了两条脉络:
| 来源 | Coroutines 的借鉴 |
|---|---|
| Future / async-await | suspend 函数让异步代码读起来像同步 |
| Rx / 响应式 | Flow 提供冷流式的数据流抽象 |
Google 将 Coroutines 作为 Android 官方推荐的异步方案,与 Jetpack(ViewModel、viewModelScope)、Room、Retrofit、Compose 深度集成。
4.2 suspend 与挂起
suspend 标记的函数可在不阻塞线程的情况下挂起协程,等待异步结果后再恢复执行:
suspend fun loadProfile(): Profile = withContext(Dispatchers.IO) {
api.fetchProfile()
}
// 调用方:看起来像同步,底层非阻塞
viewModelScope.launch {
val profile = loadProfile()
_uiState.value = UiState.Success(profile)
}挂起不等于阻塞——线程在等待期间可调度其他协程,这是与 Thread.sleep() 的本质区别。
4.3 结构化并发
Coroutines 强调 Structured Concurrency:协程有明确的作用域(coroutineScope、supervisorScope、viewModelScope),父协程取消时子协程级联取消,避免「游离任务」泄漏。
viewModelScope.launch { // 父作用域
coroutineScope { // 结构化:子任务受父管控
val a = async { fetchA() }
val b = async { fetchB() }
combine(a.await(), b.await())
}
} // ViewModel 销毁 → 全部取消这与 Rx 中手动 dispose() 形成对比——生命周期绑定更自然。
4.4 Flow:Coroutines 的响应式答案
Flow 是 Coroutines 对「数据流」的官方抽象,语义上接近 Rx 的冷 Observable:
fun search(query: Flow<String>): Flow<Result> = query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { q -> repository.search(q) }
.flowOn(Dispatchers.IO)| 对比 | RxJava | Kotlin Flow |
|---|---|---|
| 冷/热 | Observable 冷,Subject 热 | Flow 默认冷,StateFlow/SharedFlow 热 |
| 背压 | Flowable 内置 | buffer、conflate 等操作符 |
| 线程切换 | subscribeOn / observeOn | flowOn / launchIn |
| 生命周期 | 手动 Disposable | lifecycleScope、repeatOnLifecycle |
在 Jetpack 生态中,Room 的 @Query 返回 Flow,Retrofit 支持 suspend,新代码默认走 Coroutines 路径。
五、范式对比:一张表看清差异
| 维度 | Future | RxJava | Kotlin Coroutines |
|---|---|---|---|
| 核心抽象 | 一次性任务结果 | 随时间变化的数据流 | 可挂起的结构化任务 + Flow |
| 代码风格 | 命令式 + 回调/链式 | 声明式操作符组合 | 同步写法 + 少量挂起点 |
| 线程切换 | 线程池 / Executor | Scheduler | Dispatcher |
| 错误处理 | try/catch 或 exceptionally | onError 事件 | try/catch + CoroutineExceptionHandler |
| 取消机制 | 弱 | Disposable.dispose() | 结构化作用域自动取消 |
| 生命周期 | 需自行绑定 | 需 CompositeDisposable | viewModelScope / lifecycleScope |
| 学习曲线 | 低 | 高 | 中 |
| Android 生态 | 遗留代码 | 成熟第三方库 | 官方主推 |
5.1 错误传播:哲学差异
RxJava:异常不向上 throw,而是作为流的 onError 事件传播——符合「一切皆事件」的响应式哲学。未处理的 onError 会导致整个流终止且可能触发 UndeliverableException。
Coroutines:异常回到传统的 try/catch 模型,或在 CoroutineExceptionHandler 中捕获。更直观,但需注意 async 中未捕获的异常会传播到父作用域。
单元测试也需调整:Rx 用 TestScheduler / RxJava 测试规则;Coroutines 用 runTest、TestDispatcher、advanceUntilIdle。
六、选型建议
6.1 何时继续用 RxJava
- 现有大型代码库已深度 Rx 化,迁移 ROI 低
- 复杂事件流组合(多源 merge、复杂 backpressure)已有成熟 Rx 实现
- 团队 Rx 经验丰富,无强制迁移压力
6.2 何时优先 Coroutines
- 新项目或 Kotlin 为主的技术栈
- 与 Jetpack Compose、Room Flow、Retrofit suspend 集成
- 需要简洁的串行/并行异步(
async/await风格) - 希望结构化并发与生命周期自动绑定
6.3 混合策略
实践中常见 Coroutines 为主 + Rx 桥接(kotlinx-coroutines-rx2 / rx3)的渐进迁移:新功能用 suspend/Flow,旧模块通过 await() / asFlow() 逐步替换。
七、关于「要不要学 Coroutines」
不学 Coroutines 也能完成工作——掌握 RxJava 足以覆盖绝大多数异步场景。但若希望在现代 Kotlin / Android 生态中持续深入,Coroutines 已是事实标准:
- Google 官方文档、Sample、Codelab 默认 Coroutines
- 新 Jetpack 库 API 优先暴露 suspend / Flow
- 社区新文章、开源项目以 Coroutines 为主
不必因「别人都在学」而焦虑。真正值得投入的是底层思想:线程模型、调度策略、错误传播、取消语义、背压与流的生命周期。框架只是这些思想的具体实现。
八、总结
异步编程的演进,是在不断回答「代码如何与时间相处」:
- Callback / Future 让「等待结果」成为可能,但组合能力有限
- RxJava 教会我们以流的角度思考变化——订阅、变换、合并、调度
- Coroutines 教会我们以结构化的方式掌控并发——挂起、作用域、取消、Flow
Rx 强调流的连续性;Coroutines 强调结构化的并发。
掌握这两种思想,无论前端 Promise、后端 async/await,还是 Android 的线程调度,都能触类旁通。选型时不必教条——理解范式差异,结合团队现状与生态方向做 pragmatic 决策,比追新或守旧都更重要。