返回 Java / Kotlin
Java / Kotlin
12 分钟阅读

异步编程思想的演进:从 Rx 到 Kotlin Coroutines

从 Callback、Future 到 RxJava 与 Kotlin Coroutines——异步编程范式的演进脉络、设计哲学与选型依据

无论是 JavaScript 的 Promise / async-await,还是 Android / Kotlin 世界里的 Future、RxJava、Coroutines,本质上都在解决同一个问题:如何在多线程环境中,让异步逻辑可读、可组合、可测试,同时不阻塞 UI 线程。

作为开发者,理解异步机制的思想比熟悉具体 API 更重要——框架会迭代,但范式一旦掌握,迁移成本会显著降低。

异步编程思想演进总览:从 Callback 回调地狱、Future 一次性结果,到 RxJava 响应式数据流,再到 Kotlin Coroutines 结构化并发与 Flow

上图从左至右呈现异步范式的四代演进。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 在此基础上支持链式组合(thenApplythenCompose)。

特征说明
思维模型命令式:「启动任务 → 等待/回调结果」
适用场景单次异步操作、简单链式组合
局限难以表达持续变化的数据流;组合复杂时 API 冗长;取消与生命周期绑定弱

Future 强调的是任务的生命周期,而非数据随时间流动的过程。


三、RxJava:响应式编程范式

3.1 核心思想

Rx(Reactive Extensions)将异步重新定义为对随时间变化的数据流(Stream)的处理。核心类型包括:

类型语义
Observable0..N 个元素,无背压
Single恰好 1 个元素或错误
Completable仅完成信号,无数据
Flowable带背压的 Observable

「用声明式的方式描述数据变化,而不是命令式地等待结果。」

订阅者通过 subscribe() 观察流的事件:onNextonErroronComplete。操作符(mapflatMapzipmerge)以函数式方式组合、变换、合并多条流。

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-awaitsuspend 函数让异步代码读起来像同步
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:协程有明确的作用域(coroutineScopesupervisorScopeviewModelScope),父协程取消时子协程级联取消,避免「游离任务」泄漏。

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)
对比RxJavaKotlin Flow
冷/热Observable 冷,Subject 热Flow 默认冷,StateFlow/SharedFlow
背压Flowable 内置bufferconflate 等操作符
线程切换subscribeOn / observeOnflowOn / launchIn
生命周期手动 DisposablelifecycleScoperepeatOnLifecycle

在 Jetpack 生态中,Room 的 @Query 返回 Flow,Retrofit 支持 suspend,新代码默认走 Coroutines 路径。


五、范式对比:一张表看清差异

维度FutureRxJavaKotlin Coroutines
核心抽象一次性任务结果随时间变化的数据流可挂起的结构化任务 + Flow
代码风格命令式 + 回调/链式声明式操作符组合同步写法 + 少量挂起点
线程切换线程池 / ExecutorSchedulerDispatcher
错误处理try/catch 或 exceptionallyonError 事件try/catch + CoroutineExceptionHandler
取消机制Disposable.dispose()结构化作用域自动取消
生命周期需自行绑定需 CompositeDisposableviewModelScope / lifecycleScope
学习曲线
Android 生态遗留代码成熟第三方库官方主推

5.1 错误传播:哲学差异

RxJava:异常不向上 throw,而是作为流的 onError 事件传播——符合「一切皆事件」的响应式哲学。未处理的 onError 会导致整个流终止且可能触发 UndeliverableException

Coroutines:异常回到传统的 try/catch 模型,或在 CoroutineExceptionHandler 中捕获。更直观,但需注意 async 中未捕获的异常会传播到父作用域。

单元测试也需调整:Rx 用 TestScheduler / RxJava 测试规则;Coroutines 用 runTestTestDispatcheradvanceUntilIdle


六、选型建议

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 决策,比追新或守旧都更重要。

相关文章

Java / Kotlin
9 分钟
Kotlin Coroutine
以图片处理串行流水线为例,对比 Callback、RxJava 与 Coroutines 三种异步写法
Android
12 分钟
为什么要引入线程调度框架
在Java和Kotlin里面,优雅地实现异步编程模型