1. 协程基础回顾:协程是什么、为什么需要协程、协程与线程的区别、协程的基本构建块

好,咱们开始第一讲。说实话,每次带新人或者做技术分享,我都是从这一节讲起。协程这东西,刚接触时觉得玄乎,用熟了又觉得理所当然。但如果你让我用一句话说清楚——协程是轻量级的异步任务管理框架,它让异步代码写起来像同步一样自然。

1.1 协程是什么?

协程,英文叫 Coroutine。你可以把它理解成可挂起、可恢复的计算单元。什么意思呢?就是一段代码跑着跑着,可以暂停下来,等某个耗时操作(比如网络请求、数据库读写)完成了,再接着往下跑。

我刚开始学的时候,总觉得这跟线程差不多啊。其实差别大了去了。协程是协作式的,它主动让出 CPU;线程是抢占式的,由操作系统调度。说白了,协程是自己控制什么时候停、什么时候走,线程是被系统强行切换的。

核心要点:协程不是线程的替代品,而是线程的管理工具。它帮你在合适的线程上执行合适的任务,同时避免回调地狱。

1.2 为什么需要协程?

你想想看,Android 开发中我们经常遇到这样的场景:

  • 从网络拉取用户信息
  • 从本地数据库读取缓存
  • 更新 UI

这三步有先后依赖关系。如果用传统的回调方式,代码会变成这样:

api.fetchUserInfo(userId) { user ->
    db.saveUser(user) {
        runOnUiThread {
            textView.text = user.name
        }
    }
}

这就是经典的「回调地狱」。代码嵌套越来越深,逻辑越来越乱。我曾在项目里见过七层回调嵌套的代码,调试时简直想砸电脑。

协程的出现,就是为了解决这个问题。它让你用顺序写代码的方式,表达异步执行的逻辑

suspend fun loadUser(userId: String) {
    val user = api.fetchUserInfo(userId)  // 挂起,不阻塞线程
    db.saveUser(user)                      // 挂起,不阻塞线程
    textView.text = user.name              // 自动切回主线程
}

看到了吗?代码从上往下读,逻辑清清楚楚。这就是协程的魅力。

1.3 协程与线程的区别

这个问题面试常问,我也被问过很多次。我习惯用一个比喻来解释:

  • 线程就像一辆公交车,有固定的路线和站点,乘客(任务)上下车由司机(操作系统)决定。
  • 协程就像一辆出租车,乘客(任务)自己决定什么时候停、什么时候走,灵活得多。

具体区别,我整理了一张表:

对比维度 线程 协程
调度方式 操作系统抢占式调度 用户态协作式调度
创建成本 高(约 1MB 栈空间) 极低(约 1KB 内存)
切换开销 涉及内核态切换,开销大 纯用户态切换,开销极小
并发数量 几百个就吃力了 轻松上万
代码可读性 回调嵌套,难以维护 顺序编写,清晰易懂

我的经验:在 Android 中,主线程负责 UI 更新,不能阻塞。协程让你在后台线程执行耗时操作,然后自动切回主线程更新 UI。这个「自动切线程」的能力,是协程最实用的特性之一。

1.4 协程的基本构建块

协程有三个最常用的构建块:launchasyncrunBlocking。我一个个说。

1.4.1 launch:只管发射,不管结果

launch 用于启动一个不关心返回值的协程。比如记录日志、发送统计、更新缓存等。

scope.launch {
    // 这是一个协程
    val data = fetchData()   // 挂起函数
    saveToCache(data)        // 挂起函数
    // 不需要返回结果
}

我在项目中常用 launch 来处理「发后即忘」的任务。比如用户点击按钮后,我们记录一次点击事件,不需要等待结果。

1.4.2 async:要结果,就用它

当你需要协程返回一个结果时,用 async。它返回一个 Deferred 对象,你可以通过 await() 获取结果。

scope.launch {
    val deferred = async {
        fetchUserInfo(userId)  // 返回 User 对象
    }
    // 做其他事情...
    val user = deferred.await()  // 等待结果
    updateUI(user)
}

我习惯用 async 做并行任务。比如同时请求用户信息和商品列表,等两个都完成后一起处理:

scope.launch {
    val userDeferred = async { api.fetchUser() }
    val productDeferred = async { api.fetchProducts() }
    
    val user = userDeferred.await()
    val products = productDeferred.await()
    
    showData(user, products)
}

注意:千万不要在 async 块里直接调用 await(),那样就变成串行了。正确的做法是:先启动所有 async,再统一 await()

1.4.3 runBlocking:桥接世界

runBlocking 是一个特殊的构建块。它会阻塞当前线程,直到协程执行完毕。这通常只用在 main 函数或测试代码中。

fun main() {
    runBlocking {
        delay(1000)
        println("Hello from coroutine!")
    }
    // 这里会等协程执行完才执行
    println("Done")
}

我曾经在项目里看到有人把 runBlocking 用在 Android 的 Activity 里,结果导致 ANR。记住:不要在 Android 主线程使用 runBlocking,它会阻塞 UI 渲染。

1.5 知识体系总览

为了让你对本章内容有个整体印象,我画了一张图:

协程基础回顾 · 知识体系 Kotlin 协程 协程是什么 可挂起/恢复的计算单元 协作式调度 为什么需要 解决回调地狱 异步代码顺序写 与线程区别 轻量级 vs 重量级 用户态 vs 内核态 基本构建块 launch / async / runBlocking 挂起函数 / 协程作用域 核心:轻量 · 可挂起 · 顺序写异步

1.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要滥用 GlobalScope:它不会自动取消,容易造成内存泄漏。我习惯用 viewModelScope 或自定义 CoroutineScope
  • 注意协程的取消:协程取消是协作式的,需要检查 isActive 或调用 ensureActive()
  • 异常处理要到位launch 中的异常会直接抛出,async 中的异常在 await() 时抛出。建议用 try-catchCoroutineExceptionHandler 统一处理。

我的习惯:在团队里,我要求所有协程代码必须明确指定 CoroutineScopeDispatcher。这样代码可读性高,也方便 review。

好了,这一章就到这里。协程的基础概念和三个构建块,你掌握了吗?下一章我们深入聊聊挂起函数和协程作用域,这些东西在实际项目中天天用。


公众号:蓝海资料掘金营,微信deep3321