协程到底省了什么

协程不是开更多线程,而是在少量线程上挂起与恢复大量任务。挂起一个协程几乎不占线程,于是你能同时发起几十上百个网络请求,而不必为每个请求开线程。代码写成顺序样子,阻塞等待却不真的卡住线程。

协程的价值:用同步的代码结构,写出异步的执行效率,并且天然可取消、可组合。

这篇在"会用 launch"的基础上,把挂起、调度、取消与 Flow 讲透。

挂起点与 suspend

suspend 标记的函数只能在协程或另一个 suspend 里调用。它的特殊之处是可以在不阻塞线程的情况下暂停——执行到挂起点时,协程把线程让出去做别的事,等结果回来再恢复。

suspend fun fetchUser(): User {
  delay(800)                 // 挂起,不占线程
  return User("码农")
}

delay 不是 Thread.sleep:它挂起协程而不是阻塞线程。理解这一点,就理解了协程和线程池的本质区别。

调度器与作用域

调度器决定协程跑在哪类线程上:Dispatchers.Main 是主线程,适合更新界面;IO 适合网络与磁盘;Default 适合 CPU 密集计算。作用域(CoroutineScope)管理一批协程的生命周期。

常用调度器
  • Main主线程,更新 UI。
  • IO网络、数据库、文件读写。
  • Default排序、解析等 CPU 密集活。

ViewModel 里用 viewModelScope,页面销毁时自动取消其中所有协程,避免内存泄漏。

结构化并发与取消

结构化并发指协程成树状组织:父协程等所有子协程,父取消则子一并取消。async/await 用来并发并拿结果;withTimeout 给任务加超时。

viewModelScope.launch {
  val user = async(Dispatchers.IO) { fetchUser() }
  val list = async(Dispatchers.IO) { fetchList() }
  uiState = Result.Ready(user.await(), list.await())
}

两个请求并发执行,一起 await。页面销毁时 viewModelScope 取消,两个请求随之取消,不会在后台白跑。

Flow:冷流与操作符

一次返回单个值用 suspend 函数;一串随时间产出的值用 Flow。Flow 是冷流——只有被 collect 才开始执行,像一条按需打开的数据流。map、filter、flatMap 这些操作符把数据流一步步加工。

fun search(q: String): Flow<List<Item>> =
  flow { emit(api.search(q)) }
    .debounce(300)
    .distinctUntilChanged()
    .flowOn(Dispatchers.IO)
安卓协程示意:多条并行发光流线从左上向右下汇聚又分叉,交织成发光网络节点
并发与流:多条协程流线并行,数据流在节点间被加工与合并。

debounce 让连续输入只在停顿后才发请求,这正是搜索框的典型套路。