协程相关的小知识

什么是协程 (Coroutine)?

  • 协程 = 一种可以 挂起 和 恢复 的轻量级线程。
  • Kotlin 里,通过 launch {} / async {} 等构建器创建的,才是真正的 协程,是真正执行任务的实体。
  • 每个协程都有一个 Job(生命周期控制器),用来追踪它的状态(Active、Cancelled、Completed)。

比如:

runBlocking {
    launch {
        println("我是一个协程")
    }
}

这里的 launch { … } 就创建了一个 新协程。

什么是「协程作用域」?

协程作用域(CoroutineScope) = 协程的运行环境,用来管理协程的生命周期。
一个协程作用域必须要有一个 CoroutineContext,里面通常至少包含两个关键元素:

  • Job:用于控制作用域内所有子协程的生命周期(取消/完成等)。
  • Dispatcher:决定协程运行在哪个线程上。

换句话说:
👉 协程作用域就是「一片地盘」,在这片地盘上你可以创建协程,它们都归这个地盘的 Job 管。
在作用域里,你才能启动协程。

runBlocking { // runBlocking 创建了一个作用域
    // 在这个作用域里才能 launch 协程
    launch {
        println("我运行在 runBlocking 的作用域里")
    }
}

1. 协程作用域的来源

(1)直接实现 CoroutineScope 接口
比如你自己写一个类来持有作用域:

class MyScope: CoroutineScope {
    override val coroutineContext = Job() + Dispatchers.Default
}

(2)内置的作用域构造器

这些构造器会创建一个作用域,供你在代码块里启动协程:

  • coroutineScope {}

    • 挂起函数,创建一个普通作用域。
    • 子协程失败会取消整个作用域。
  • supervisorScope {}

    • 挂起函数,创建一个监督作用域。
    • 子协程失败不会影响兄弟协程。

(3)顶层作用域构建器(其实是协程启动器)

  • runBlocking {}

    • 会启动一个新的协程,并阻塞当前线程直到完成。
    • 常用于 main() 或测试环境。
  • GlobalScope

    • 全局作用域,不依赖任何 Job → 协程和应用进程一样长命。
    • 一般不推荐直接用。

总结:

  • 「作用域」本身 = 一个 协程上下文 + 生命周期管理机制。
  • coroutineScope / supervisorScope 并不是协程,而是挂起函数,在当前协程里建立一个新的子作用域。
  • runBlocking / GlobalScope 是“顶层作用域”,一般用来作为整个应用或测试的入口。
  • 没有作用域,就不能启动协程;作用域像“容器”,协程像“任务单元”。

什么是协程的上下文(CoroutineContext)

在 Kotlin 里,协程的上下文(CoroutineContext) 就像一个“运行环境”,
它保存了协程执行所需的一组元素,告诉协程 在哪里运行、叫什么名字、如何处理异常 等。

可以理解成:协程的“身份证 + 执行规则集合”。
CoroutineContext 是一个 元素集合(CoroutineContext.Element)。包含的元素有:

上下文元素作用示例
Job协程的生命周期,控制启动/取消/完成launch { ... } 会自动带一个 Job
CoroutineDispatcher指定协程运行的线程(调度器)Dispatchers.IO / Dispatchers.Default / Dispatchers.Main
CoroutineName给协程起名字,方便调试和日志CoroutineName("Worker-1")
CoroutineExceptionHandler统一处理未捕获异常CoroutineExceptionHandler { _, e -> ... }
import kotlinx.coroutines.*

fun main(): kotlin.Unit = runBlocking {
    launch(context = Dispatchers.IO + CoroutineName("MyCoroutine")) {
        println("协程名字: ${coroutineContext[CoroutineName]}")
        println("调度器: ${coroutineContext[CoroutineDispatcher]}")
        println("Job: ${coroutineContext[Job]}")
    }
}

输出类似:

协程名字: CoroutineName(MyCoroutine)
调度器: Dispatchers.IO
Job: StandaloneCoroutine{Active}@8dd9c13

总结:

  • 协程上下文 = 协程的运行环境配置(由多个元素组成)。
  • 常见元素有:Job(生命周期)、Dispatcher(线程调度)、Name(调试用)、ExceptionHandler(错误处理)。
  • 可以通过 coroutineContext 访问当前协程的上下文。
  • withContext 就是用来切换上下文的。

1. CoroutineContext 和 CoroutineScope的关系

协程上下文(CoroutineContext) 和 协程作用域(CoroutineScope) 关系很紧密,但不是同一个概念。
(1) 协程上下文(CoroutineContext)

  • 定义:协程运行的环境配置,包含 Job、Dispatcher、CoroutineName、ExceptionHandler 等元素。

  • 它是一个“配置集合”,不会独立存在,一般是 依附在协程或者作用域上。

  • 可以用 coroutineContext 访问。

(2) 协程作用域(CoroutineScope)
定义:协程作用域就是一个接口,它持有一个 CoroutineContext,并提供 launch {}、async {} 等函数来启动协程。

  • 所以 作用域 = 上下文 + 协程构建器。
  • 每个作用域内部的子协程都会继承它的 CoroutineContext。

两者的关系
CoroutineScope 是对 CoroutineContext 的封装。
当你创建一个作用域时,你实际上就是给它配置了一个上下文。
如果在一个作用域里再 launch 子协程,子协程会继承父作用域的上下文, 例如:Dispatcher、CoroutineName

import kotlinx.coroutines.*

fun main(): kotlin.Unit = runBlocking {
    val scope = CoroutineScope(Dispatchers.IO + CoroutineName("MyScope"))

    val job = scope.launch {
        // 父协程
        println("父协程 上下文对象:$coroutineContext, " +
                "协程名字: ${coroutineContext[CoroutineName]}, " +
                "调度器: ${coroutineContext[CoroutineDispatcher]}, " +
                "Job: ${coroutineContext[Job]}")

        // 子协程
        launch {
            println("子协程 上下文对象:$coroutineContext, " +
                    "协程名字: ${coroutineContext[CoroutineName]}, " +
                    "调度器: ${coroutineContext[CoroutineDispatcher]}, " +
                    "Job: ${coroutineContext[Job]}")
        }
    }

    job.join()
}

输出结果:

父协程 上下文对象:[CoroutineName(MyScope), StandaloneCoroutine{Active}@982e489, Dispatchers.IO], 协程名字: CoroutineName(MyScope), 调度器: Dispatchers.IO, Job: StandaloneCoroutine{Active}@982e489
子协程 上下文对象:[CoroutineName(MyScope), StandaloneCoroutine{Active}@4af8a9f0, Dispatchers.IO], 协程名字: CoroutineName(MyScope), 调度器: Dispatchers.IO, Job: StandaloneCoroutine{Active}@4af8a9f0

可以看到每个 launch 都会创建新的 Job

  • Job 是协程上下文的一部分,代表协程的生命周期。

  • 当你调用 launch,会自动生成一个新的 Job 并加到上下文里。

子协程会继承父协程的上下文,但会 替换 Job 元素,从而保证每个协程有自己独立的生命周期。

👉 所以 上下文不是“同一个对象”,但它们共享了大部分元素(比如 Dispatcher, CoroutineName)。

什么是Job?

在 Kotlin 协程中,Job 是协程的句柄(handle),代表一个可取消、可完成的后台任务。
你可以把它理解成 协程的生命周期管理器。

每次你用launch {}async {} 启动一个协程时,都会返回一个 Job(或者 Deferred<T>,它是 Job 的子类)。

Job 的主要功能

  • 生命周期控制

    • isActive → 协程是否正在运行
    • isCompleted → 协程是否已经完成
    • isCancelled → 协程是否被取消
  • 取消协程

    • job.cancel() → 发送取消信号
    • job.cancelAndJoin() → 取消并等待协程完全退出
  • 等待协程完成

    • job.join() → 挂起当前协程,直到目标协程结束
  • 层级关系

    • 父协程的 Job 会管理所有子协程的 Job;

    • 默认情况下,子协程失败会取消整个父 Job。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job: Job = launch {
        repeat(5) { i ->
            println("协程执行 $i")
            delay(300)
        }
    }

    delay(700)
    println("取消协程...")
    job.cancelAndJoin()  // 取消并等待退出
    println("主协程结束")
}

可能输出:

协程执行 0
协程执行 1
协程执行 2
取消协程…
主协程结束

1. 父 Job 和 子 Job 的关系

在 Kotlin 协程里,每个 launch 或 async 启动的新协程都会 生成一个新的 Job。

  • 这个 Job 会和启动它的协程的 Job 建立 父子关系。

  • 因此,协程之间形成了一棵 Job 树(层级结构)。

父子 Job 的特性

(1) 生命周期管理

  • 如果 父 Job 被取消,它的所有子 Job 也会被递归取消。

  • 相反,子 Job 取消不会影响父 Job。

  • 父协程要等所有子协程完成后,才会进入完成状态。

import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    val parentJob = launch {
        val childJob = launch {
            try {
                delay(1000)
                println("子任务完成")
            } finally {
                println("子任务被取消")
            }
        }

        delay(200)
        println("取消父任务")
        this.cancel() // 取消父任务
    }

    parentJob.join()
    println("父任务结束")
}

结果:

取消父任务
子任务被取消
父任务结束

可以看到取消父job的时候,也会一起取消子job.
相反如果只是取消子job, 那么父job 还会继续运行, 例如:

import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    val parentJob = launch {
        val childJob = launch {
            try {
                delay(1000)
                println("子任务完成")
            } finally {
                println("子任务被取消")
            }
        }

        delay(200)
        childJob.cancel() // 取消子job

        delay(1200)
        println("父任务正常执行")
    }

    parentJob.join()
    println("父任务结束")
}

输出:

子任务被取消
父任务正常执行
父任务结束

join 和 cancelAndJoin

2. join() 函数的作用

作用:等待协程执行完成(无论正常完成还是异常结束)。

不会主动取消协程,只是挂起调用方,直到目标协程的生命周期结束。

👉 示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(1000)
        println("子协程完成")
    }
    println("等待子协程...")
    job.join() // 仅仅等待
    println("主协程继续")
}

输出结果:

等待子协程…
子协程完成
主协程继续

3. cancelAndJoin() 函数的作用

作用:先取消协程(发送取消信号),再等待它结束。

等价于:

job.cancel()
job.join()

常用于“干掉某个任务,然后等它完全退出”。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(5) {
                println("子协程执行 $it")
                delay(500) // 当job被cancel的时候,这里会抛出CancellationException异常,然后结束当前job
            }
        } finally {
            println("子协程清理资源")
        }
    }
    delay(1200) // 让子协程跑一会
    println("取消子协程...")
    job.cancelAndJoin()
    println("主协程继续")
}

输出:

子协程执行 0
子协程执行 1
子协程执行 2
取消子协程…
子协程清理资源
主协程继续

什么是 Dispatchers?

在 Kotlin 协程中,Dispatchers 决定协程运行在哪个线程或线程池上。
它相当于协程的“调度器”(Scheduler),帮助我们指定任务的执行环境。
Kotlin 提供了几个常用的内置调度器:

调度器说明适用场景
Dispatchers.Default默认调度器,基于共享的后台线程池(CPU 核心数大小)。CPU 密集型任务(计算、排序、解析)。
Dispatchers.IO专门优化了 I/O 操作的线程池(比 Default 更大)。I/O 密集型任务(读写文件、网络请求、数据库操作)。
Dispatchers.MainAndroid 或 UI 应用里的主线程调度器。更新 UI(仅 Android/JavaFX/Compose 等支持)。
Dispatchers.Unconfined不指定具体线程,在哪启动就在哪运行,直到遇到挂起点后,恢复可能切到别的线程。调试、测试用;少用于生产。

使用示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        println("运行在 Default: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.IO) {
        println("运行在 IO: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.Unconfined) {
        println("运行在 Unconfined: ${Thread.currentThread().name}")
        delay(100)
        println("恢复后可能在别的线程: ${Thread.currentThread().name}")
    }
}

可能输出(不同机器略有不同):

运行在 Default: DefaultDispatcher-worker-1
运行在 IO: DefaultDispatcher-worker-2
运行在 Unconfined: main
恢复后可能在别的线程: kotlinx.coroutines.DefaultExecutor

1.自定义Dispatchers

在 Kotlin 协程里,内置的 Dispatchers.Default / IO / Main / Unconfined 已经覆盖了大多数场景,但有时候你可能需要更精细的调度,例如:

  • 限制协程运行的线程数;
  • 使用单线程执行某些任务;
  • 和已有的线程池/Executor 对接。

这就需要 自定义 Dispatcher。

Dispatchers 本质是一个 CoroutineDispatcher
我们可以通过 asCoroutineDispatcher() 方法,把一个 Executor 或 ExecutorService 转换为协程调度器。

val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

2. 固定大小线程池

import kotlinx.coroutines.*
import java.util.concurrent.*

fun main(): kotlin.Unit = runBlocking {
    // 创建一个固定 2 个线程的线程池
    val myDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()

    val jobs = List(5) { i ->
        launch(myDispatcher) {
            println("任务 $i 在线程: ${Thread.currentThread().name}")
            delay(500)
        }
    }
    jobs.joinAll()

    // 注意:用完一定要关闭,否则程序不会退出
    (myDispatcher.executor as ExecutorService).shutdown()
}

输出可能是:

任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-2
任务 2 在线程: pool-1-thread-2
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-2

3. 单线程调度器(类似 Android Main)

import kotlinx.coroutines.*
import java.util.concurrent.*

val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

fun main(): Unit = runBlocking {
    // 启动多个协程并发执行
    val jobs = List(5) { i ->
        launch(singleThreadDispatcher) {
            println("任务 $i 在线程: ${Thread.currentThread().name}")
            delay(200)
            println("任务 $i 结束 在线程: ${Thread.currentThread().name}")
        }
    }
    jobs.joinAll()

    // 关闭 dispatcher,防止程序挂起
    singleThreadDispatcher.close()
}

预期输出(不同机器线程名可能略有差别)

任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-1
任务 2 在线程: pool-1-thread-1
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-1
任务 0 结束 在线程: pool-1-thread-1
任务 1 结束 在线程: pool-1-thread-1
任务 2 结束 在线程: pool-1-thread-1
任务 3 结束 在线程: pool-1-thread-1
任务 4 结束 在线程: pool-1-thread-1

说明:

  • 所有协程都运行在 同一个线程:pool-1-thread-1;
  • 尽管有多个协程并发执行,但调度器保证它们串行地使用同一个线程(即单线程语义);

如果你改成 Executors.newFixedThreadPool(2), 输出就会出现 pool-1-thread-1 和 pool-1-thread-2 两个不同线程。

任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-2
任务 2 在线程: pool-1-thread-1
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-2
任务 1 结束 在线程: pool-1-thread-1
任务 0 结束 在线程: pool-1-thread-2
任务 2 结束 在线程: pool-1-thread-1
任务 3 结束 在线程: pool-1-thread-1
任务 4 结束 在线程: pool-1-thread-1

4. withContext

withContext 通常是和 Dispatchers 搭配使用的,因为它的主要作用就是 切换协程运行的上下文(Context),而上下文里最常见需要切换的就是 调度器(Dispatcher)。

suspend fun showData() {
    val data = withContext(Dispatchers.IO) {
        // 网络/数据库
        "用户数据"
    }

    withContext(Dispatchers.Main) {
        println("运行在主线程: ${Thread.currentThread().name}")
        // 更新 UI
        // textView.text = data
    }
}

什么是结构化并发?

结构化并发的核心思想:

  • 协程必须在某个 作用域(scope) 中运行;

  • 子协程的生命周期受父协程作用域管理;

  • 父协程不会结束,直到所有子协程都结束;

  • 子协程异常会向上传递,默认会取消整个作用域。

  • 这样可以避免“野生协程”泄漏(比如启动了后台协程却没人管,导致内存泄漏或异常丢失)。

coroutineScope 和 supervisorScope 都是结构化并发的一部分

很多人一开始会觉得:既然 supervisorScope 里的子协程不会互相传播异常,那是不是就“不结构化”了?其实不是。

它和 coroutineScope 一样,都会保证:

  • 子协程都属于父作用域;

  • 父作用域不会结束,直到子协程全部完成(不管是正常完成还是异常结束);

  • 父作用域负责等待并管理子协程的生命周期。

只是 异常传播策略不同:

  • coroutineScope → 子协程异常会取消整个作用域。

  • supervisorScope → 子协程异常只会取消自己,不会影响兄弟协程,但父作用域仍然会等待所有子协程完成才结束。

什么是非结构化并发?

非结构化并发 = 启动的协程 没有被作用域管理,它的生命周期不受调用方控制。

这意味着:

  • 协程可能比启动它的代码活得更久(内存泄漏风险)。

  • 协程失败时,调用方可能感知不到异常。

  • 协程的结束时机不可控,难以保证资源正确释放。

例如:

fun main() {
    GlobalScope.launch { // 非结构化并发:脱离当前函数作用域
        delay(1000)
        println("子协程完成")
    }
    println("main函数结束")
}

运行结果很可能只输出:

main函数结束

因为 main 函数结束时,协程还没完成,进程就直接退出了。

这里的问题就是:协程的生命周期 和 main 函数完全脱节 → 这就是非结构化并发。

和结构化并发的对比

特点非结构化并发结构化并发
生命周期脱离调用方作用域嵌套在调用方作用域中
管理方式协程“野生运行”由父作用域 Job 管理
安全性容易泄漏,异常难追踪安全可控,异常传播有规则
示例GlobalScope.launch {}runBlocking + launch / coroutineScope / supervisorScope

协程的异常的传播规则

在 Kotlin 协程中,子协程的异常是否会导致父协程结束,取决于你使用的协程构建器和作用域。默认情况下确实会传播异常,导致父协程结束。

1. 默认情况(launch / async + 普通 CoroutineScope)

如果在 结构化并发(例如 coroutineScope {} 或 launch {})中启动子协程:

子协程抛出未捕获的异常 → 异常会向上传递 → 会取消整个父协程作用域,从而结束父协程及其所有兄弟协程。

👉 举个例子:

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("最外层父协程开始")
    launch {
        println("里层父协程开始")
        launch {
            println("子协程1开始")
            throw RuntimeException("子协程1异常")
        }
        launch {
            delay(1000)
            println("子协程2完成") // 永远不会执行
        }
        delay(1000)
        println("里层父协程完成") // 永远不会执行
    }
    println("最外层父协程完成")
}

结果:

最外层父协程开始
最外层父协程完成
里层父协程开始
子协程1开始

Exception in thread “main” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1$1KaTeX parse error: Expected group after '_' at position 371: …ines.BuildersKt_̲_BuildersKt.run…default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:22)
at MainKt.main(Main.kt)

子协程1异常导致整个父作用域取消 → 子协程2也被取消。

协程层级关系如下:

runBlocking (顶层父协程)
└── launch (里层父协程)
├── launch { 子协程1 }
└── launch { 子协程2 }

  • 最外层 runBlocking 启动一个协程,挂在 main 线程。
  • 里面第一个 launch { … } 是 父协程。
  • 父协程里再启动两个子协程(子协程1、子协程2)。

异常传播机制(普通 launch):

默认规则:在一个普通作用域里,如果任意一个子协程失败,它会:

  • 取消它的兄弟协程们(这里子协程2会被取消,来不及执行打印语句)。
  • 取消父协程本身(所以父协程自己的 delay 和 println 也不会运行)。
  • 因为父协程被取消,所以最终 子协程2完成 和 父协程完成 都不会输出。

2. 使用 supervisorScope

如果你希望子协程的异常不影响父协程和兄弟协程,需要使用 supervisorScope 或 SupervisorJob.

supervisorScope {} 是一个挂起函数,和 coroutineScope {} 类似,但作用域内的异常传播规则不同:

  • 子协程失败只会取消自己;

  • 兄弟协程继续运行;

  • 父作用域会等待所有子协程结束后才返回。

import kotlinx.coroutines.*
fun main() = runBlocking {
    println("父协程开始")
    supervisorScope {
        println("父作用域开始")
        launch {
            println("子协程1开始")
            throw RuntimeException("子协程1异常")
        }
        launch {
            delay(1000)
            println("子协程2完成") // 仍然会继续执行
        }
        delay(1000)
        println("父作用域结束") // 仍然会继续执行
    }
    println("父协程结束")
}

结果:

父协程开始
父作用域开始
子协程1开始
Exception in thread “main” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1$1KaTeX parse error: Expected group after '_' at position 371: …ines.BuildersKt_̲_BuildersKt.run…default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:4)
at MainKt.main(Main.kt)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@326de728, BlockingEventLoop@25618e91]
父作用域结束
子协程2完成
父协程结束

这里的协程层级关系 解释一下

  1. runBlocking { ... }
  • 是最顶层的父协程。最外层,运行在 main 线程中。
  • 会启动一个 最顶层的协程,并阻塞当前线程直到它内部的代码执行完。
  1. supervisorScope { ... }
  • 它不是新的协程,而是一个作用域构造器(CoroutineScope),继承了 runBlocking 的上下文(Job、Dispatcher 等),所以它挂靠在 runBlocking 的 Job 下。
  • 它的生命周期受 runBlocking 管理,但它是 launch 子协程们的直接父作用域。
  • 它为里面的 launch {} 提供了“父作用域”的角色。
  • 只是一个“环境”,它本身不是协程,不会自己执行,只是用来管理上下文和子协程的生命周期。
  • 它的父协程是runBlocking
  1. launch { ... }(子协程1、子协程2)
  • 这两个协程是 新的协程。
  • 它们的 父作用域 是 supervisorScope。也会继承 coroutineScope 的上下文 → 其实最终还是挂在 runBlocking 的大 Job 树下
  • 它们的 父协程 其实就是 runBlocking,因为 supervisorScope 本身并不是协程。
  • 如果用了 supervisorScope,其中一个子协程失败(比如子协程1抛异常),不会影响到另一个(子协程2能继续执行)。

对应的关系层级如下:

runBlocking (顶层父协程)
│
└── supervisorScope (作用域,直接父作用域)
      ├── launch { 子协程1 }
      └── launch { 子协程2 }

3. 使用SupervisorJob

SupervisorJob 是一种特殊的 Job,不会将子协程的失败传播给父作用域。

子协程失败只会取消自己,兄弟协程和父作用域都不受影响。

import kotlinx.coroutines.*

fun main() = runBlocking {
    // 父作用域带 SupervisorJob
    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    scope.launch {
        println("子协程1开始")
        delay(500)
        throw RuntimeException("子协程1异常")
    }

    scope.launch {
        println("子协程2完成") // 正常执行
    }

    scope.launch {
        try {
            delay(3000)
            println("子协程3完成") // 会被scope.cancel 取消
        } catch (e: CancellationException) {
            println("子协程3被取消")
        }
    }

    delay(1500) // 等待子协程完成
    println("父协程依然存活: ${scope.isActive}")
    scope.cancel() // 最后手动取消父作用域
}

结果:

子协程1开始
子协程2完成
Exception in thread “DefaultDispatcher-worker-3” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1 1. i n v o k e S u s p e n d ( M a i n . k t : 31 ) a t k o t l i n . c o r o u t i n e s . j v m . i n t e r n a l . B a s e C o n t i n u a t i o n I m p l . r e s u m e W i t h ( C o n t i n u a t i o n I m p l . k t : 33 ) a t k o t l i n x . c o r o u t i n e s . D i s p a t c h e d T a s k . r u n ( D i s p a t c h e d T a s k . k t : 100 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r . r u n S a f e l y ( C o r o u t i n e S c h e d u l e r . k t : 586 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r 1.invokeSuspend(Main.kt:31) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586) at kotlinx.coroutines.scheduling.CoroutineScheduler 1.invokeSuspend(Main.kt:31)atkotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)atkotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)atkotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)atkotlinx.coroutines.scheduling.CoroutineSchedulerWorker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler W o r k e r . r u n W o r k e r ( C o r o u t i n e S c h e d u l e r . k t : 717 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r Worker.runWorker(CoroutineScheduler.kt:717) at kotlinx.coroutines.scheduling.CoroutineScheduler Worker.runWorker(CoroutineScheduler.kt:717)atkotlinx.coroutines.scheduling.CoroutineSchedulerWorker.run(CoroutineScheduler.kt:704)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4ccdae72, Dispatchers.Default]
父协程依然存活: true
子协程3被取消

可以看到兄弟协程2和父协程仍然继续执行, 同时scope.cancel会让正在等待(delay)的子协程抛出CancellationException异常, 导致兄弟协程3结束.

什么是挂起函数(suspend)

编译器把 suspend 函数编译成可中断/可恢复的状态机;在挂起点(如 delay/withContext/任意 suspend)释放线程,稍后在同或不同线程恢复。

要点:

  • 只能在协程或其它 suspend 里调用

  • 不阻塞线程,但能“阻塞”协程的执行流程(挂起)

    • 阻塞 (block):线程停在那里干等,不能干别的
    • 挂起 (suspend):协程让出线程,调度器可以在这段时间调度别的协程继续执行

要“证明”协程是挂起而不是阻塞,可以让多个协程并发运行,并观察它们共享同一个线程,且同时完成。

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("start @ ${Thread.currentThread().name}")

    // 启动两个协程在同一个线程
    launch {
        delay(5000) // 挂起,不占用线程
        println("Task1 done @ ${Thread.currentThread().name}")
    }
    launch {
        delay(5000) // 同样挂起
        println("Task2 done @ ${Thread.currentThread().name}")
    }

    println("Launched tasks @ ${Thread.currentThread().name}")
}

输出:

start @ main
Launched tasks @ main
Task1 done @ main
Task2 done @ main

👉 两个协程在 同一个线程 (main) 执行,且都能在 5 秒后完成。如果 delay 真的是阻塞,它会卡死线程,可以看到前2条日志是先打印的, 所以并不会阻塞线程(main)的执行。 而后面2条日志几乎是同一时刻打印的, 彼此并不会互相等待, 他们是并发执行的.

CoroutineExceptionHandler

CoroutineExceptionHandler 是 Kotlin 协程(Coroutines)中的一个异常处理机制,用于处理 未捕获的协程异常。它的作用和使用场景可以总结如下:

(1) 基本作用
在 Kotlin 协程中,如果一个协程抛出异常,而异常没有被 try-catch 捕获,通常会导致协程所在的 CoroutineScope 的父协程或者 SupervisorJob 被取消。CoroutineExceptionHandler 可以:

  • 捕获协程中未处理的异常

  • 进行日志记录、上报或者自定义处理

  • 防止程序直接崩溃(在一些情况下)

⚠️ 注意:它只处理 根协程(顶层协程)中的异常,不会捕获通过 async 返回的异常(需要通过 await 显式捕获)。

所谓顶层协程(Top-level coroutine)是指:直接在某个 CoroutineScope 下启动,没有父协程(或者父协程不会处理异常)的协程。

  • 异常在顶层协程中抛出时,如果没有捕获,就会通过 CoroutineExceptionHandler 处理。
  • GlobalScope.launch 是典型的顶层协程。
  • 异常无法向上层传播(没有父协程),所以会被 CoroutineExceptionHandler 捕获。
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 创建一个异常处理器
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }

    // launch 协程,挂载异常处理器
    val job = GlobalScope.launch(context = handler) {
        println("Throwing exception from launch")
        throw AssertionError("Oops!")
    }

    job.join()
    println("Done")
}

输出结果:

Throwing exception from launch
Caught java.lang.AssertionError: Oops!
Done

关键点

  • 只捕获未处理的异常
  • 如果协程内部已经用 try-catch 捕获,CoroutineExceptionHandler 不会被调用。
  • 对 launch 有效,对 async 无效
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 创建一个异常处理器
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }

    val deferred = GlobalScope.async(handler) {
        throw AssertionError("Fail in async")
    }
    deferred.await() // handler 不会捕获异常 这里还是会抛出异常
    println("Done")
}

结果:

Exception in thread “main” java.lang.AssertionError: Fail in async
at MainKt$main 1 1 1deferred 1. i n v o k e S u s p e n d ( M a i n . k t : 76 ) a t k o t l i n . c o r o u t i n e s . j v m . i n t e r n a l . B a s e C o n t i n u a t i o n I m p l . r e s u m e W i t h ( C o n t i n u a t i o n I m p l . k t : 33 ) a t k o t l i n x . c o r o u t i n e s . D i s p a t c h e d T a s k . r u n ( D i s p a t c h e d T a s k . k t : 100 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r . r u n S a f e l y ( C o r o u t i n e S c h e d u l e r . k t : 586 ) a t > k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r 1.invokeSuspend(Main.kt:76) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586) at >kotlinx.coroutines.scheduling.CoroutineScheduler 1.invokeSuspend(Main.kt:76)atkotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)atkotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)atkotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)at>kotlinx.coroutines.scheduling.CoroutineSchedulerWorker.executeTask(CoroutineScheduler.kt>:829)
at >kotlinx.coroutines.scheduling.CoroutineScheduler W o r k e r . r u n W o r k e r ( C o r o u t i n e S c h e d u l e r . k t : 7 > 17 ) a t > k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r Worker.runWorker(CoroutineScheduler.kt:7>17) at >kotlinx.coroutines.scheduling.CoroutineScheduler Worker.runWorker(CoroutineScheduler.kt:7>17)at>kotlinx.coroutines.scheduling.CoroutineSchedulerWorker.run(CoroutineScheduler.kt:704)

协程的取消机制

1. 协作取消

Kotlin 协程是 协作式取消 的:协程本身不会被强制停止,而是通过 挂起点检查取消状态 来响应取消。

主要相关 API:

  • Job.cancel():请求取消协程。

  • Job.cancelAndJoin():请求取消并等待协程完成。

  • CoroutineScope.isActive:检查协程是否还活跃。

  • withContext(NonCancellable):在取消的情况下仍然执行关键操作。

所谓协作式就是协程的取消需要协作取消, 它并不是立即硬性停止。

  • 当你调用 job.cancel() 或者 coroutineScope.cancel() 时,它只会 发送取消信号,标记协程为“已取消”。
  • 协程本身仍然会继续执行,直到遇到检查取消状态的地方如:
    • delay()、yield()、withContext() 等挂起函数,它们会自动抛出 CancellationException
    • 或者你手动调用 coroutineContext.ensureActive()
      也就是说,如果你的协程里没有挂起点,也没有调用 ensureActive(),协程是不会立刻停止的。

在 Kotlin 协程中,协作式取消依赖 挂起点(suspending point)来检查协程是否被取消。也就是说,只有遇到这些挂起点,协程才会响应取消。常见的协作取消挂起点 API 列表:

API / 函数说明
delay(timeMillis)延迟指定时间,遇到取消会抛出 CancellationException
yield()暂停当前协程,给其他协程机会运行,同时检查取消
withContext(context) { ... }切换上下文时会检查取消状态
await() (Deferred)等待异步任务结果,如果协程被取消会抛出异常
join() / joinAll()等待协程完成,同时响应取消
select { ... }等待多个挂起函数完成,协程取消时会终止选择
Channelreceive() / send()通道操作是挂起点,协程被取消会立即抛异常
Flow.collect { ... }收集 Flow 元素时挂起,会响应取消
Mutex.withLock { ... }获取锁是挂起点,挂起期间会响应取消
Semaphore.acquire()获取许可时挂起,会响应取消
withTimeout / withTimeoutOrNull超时挂起,会响应取消
suspendCancellableCoroutine { ... }自定义挂起函数时,如果使用该 API,会自动支持协作取消

2. NonCancellable

NonCancellable 是一个 特殊的 CoroutineContext。

用途:在协程取消后仍然保证某些关键操作执行(如清理资源、写日志、提交事务)。

典型场景:

  • finally 块中的清理操作

  • 在取消状态下执行 IO 或数据库操作

NonCancellable 只能用于少量关键操作,不宜滥用,否则会阻塞取消逻辑。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(5) { i ->
                println("Working $i ...")
                delay(500)
            }
        } finally {
            // 即使协程被取消,这里也会执行
            withContext(NonCancellable) {
                println("Cleaning up...")
                delay(1000)  // 模拟清理耗时操作
                println("Cleanup done")
            }
        }
    }

    delay(1100)
    println("Cancelling job")
    job.cancelAndJoin()
    println("Main done")
}

输出:

Working 0 …
Working 1 …
Working 2 …
Cancelling job
Cleaning up…
Cleanup done
Main done

说明:

  • finally 内的代码会在协程取消后执行。
  • 使用 withContext(NonCancellable) 可以保证 即使取消,关键逻辑仍会完成。

协程并发控制

1. async/await

说明:

  • async 用于启动并发协程,并返回 Deferred 对象。

  • await() 获取结果,遇到异常会抛出。

  • 适用于多个任务并行执行后合并结果的场景。

  • 与 launch 不同,async 可以返回结果。

特点:

  • 异步并发执行,提高性能。

  • 异常需要在 await() 时捕获。

import kotlinx.coroutines.*

suspend fun api1(): Int { delay(200); return 1 }
suspend fun api2(): Int { delay(300); return 2 }

fun main() = runBlocking {
    val a = async { api1() }
    val b = async { api2() }
    println("sum = ${a.await() + b.await()}") // sum = 3
}

2. Mutex(协程友好锁)

说明:

  • Mutex 是协程专用的互斥锁。

  • withLock { … } 会挂起当前协程,直到获得锁。

  • 避免传统锁阻塞线程,更适合协程并发场景。

适用场景:

  • 多协程同时修改共享资源时保证线程安全。
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*

fun main() = runBlocking {
    val mutex = Mutex()
    var counter = 0

    coroutineScope {
        repeat(1_000) {
            launch(Dispatchers.Default) {
                repeat(100) {
                    mutex.withLock { counter++ }
                }
            }
        }
    }
    println("counter = $counter") // 100_000
}

3. Semaphore(限流 / 限并发)

说明:

  • Semaphore 控制同时运行的协程数量(限流)。

  • permits 表示同时允许的协程数量。

  • acquire() 获取许可,release() 释放许可。

适用场景:

限制对某些资源的并发访问,例如网络请求、数据库连接池。

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*

fun main() = runBlocking {
    val sem = Semaphore(permits = 4) // 定义最大并发4个任务
    val tasks = (1..10).map { i ->
        launch(Dispatchers.Default) {
            sem.acquire() // 每次开始任务就占用一个Semaphore
            try {
                println("run $i @ ${Thread.currentThread().name}")
                delay(200)
            } finally {
                sem.release() // 每次调用一次就释放一个Semaphore, 否则4个都占用完就会停在sem.acquire()了
            }
        }
    }
    tasks.joinAll()
}

输出结果:

run 1 @ DefaultDispatcher-worker-1
run 2 @ DefaultDispatcher-worker-2
run 3 @ DefaultDispatcher-worker-3
run 4 @ DefaultDispatcher-worker-4
run 7 @ DefaultDispatcher-worker-5
run 5 @ DefaultDispatcher-worker-7
run 6 @ DefaultDispatcher-worker-6
run 8 @ DefaultDispatcher-worker-8
run 9 @ DefaultDispatcher-worker-6
run 10 @ DefaultDispatcher-worker-1

4. 原子变量(轻量计数)

说明:

  • AtomicInteger 提供原子操作,线程安全。

  • incrementAndGet() 保证计数操作不会丢失。

  • 比 Mutex 更轻量,适合简单计数器。

适用场景:

  • 高并发下的计数、累加、标记操作。

  • 不需要复杂锁定逻辑时使用。

来看一个错误的并发操作:

import kotlinx.coroutines.*

fun main() = runBlocking {
    var counter = 0
    coroutineScope {
        repeat(1_000) {
            launch(Dispatchers.Default) {
                repeat(100) { counter++ }
            }
        }
    }
    println("counter = $counter")
}

输出结果:

counter = 96813

结果和预想的100000 不符合, 正确的做法如下:

import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*

fun main() = runBlocking {
    val c = AtomicInteger(0)
    coroutineScope {
        repeat(1_000) {
            launch(Dispatchers.Default) {
                repeat(100) { c.incrementAndGet() }
            }
        }
    }
    println("counter = ${c.get()}") // 100000
}

AtomicInteger 的常用方法表格,包含方法、功能说明和返回值类型:

方法功能说明返回值
get()获取当前值当前整数值
set(newValue: Int)设置新值Unit
lazySet(newValue: Int)延迟写入新值,对性能优化有用Unit
getAndSet(newValue: Int)获取当前值,然后设置为新值原值
incrementAndGet()自增 1,并返回自增后的值新值
getAndIncrement()获取当前值,然后自增 1原值
decrementAndGet()自减 1,并返回自减后的值新值
getAndDecrement()获取当前值,然后自减 1原值
addAndGet(delta: Int)增加指定值,并返回新值新值
getAndAdd(delta: Int)获取当前值,然后增加指定值原值
compareAndSet(expect: Int, update: Int)如果当前值等于期望值 expect,则更新为 updateBoolean(是否成功)
getAndUpdate(updateFunction: (Int) -> Int)根据函数更新值,返回原值原值
updateAndGet(updateFunction: (Int) -> Int)根据函数更新值,返回新值新值
getAndAccumulate(x: Int, accumulator: (Int, Int) -> Int)使用二元函数更新值,返回原值原值
accumulateAndGet(x: Int, accumulator: (Int, Int) -> Int)使用二元函数更新值,返回新值新值

5. limitedParallelism

limitedParallelism(n) 可以限制 同一个 CoroutineDispatcher/CoroutineScope 下协程的最大并发数为 n。

它返回一个新的 CoroutineDispatcher,在这个 dispatcher 上启动的协程同时最多只会有 n 个在运行,其余协程会挂起等待。

简单说,它是一种 轻量级的限流工具,不需要手动 acquire/release

import kotlinx.coroutines.*

fun main() = runBlocking {
    // 创建一个限制并行度为 3 的调度器
    val limitedDispatcher = Dispatchers.Default.limitedParallelism(3)

    val jobs = (1..10).map { i ->
        launch(limitedDispatcher) {
            println("Start task $i @ ${Thread.currentThread().name}")
            delay(1500) // 模拟耗时操作
            println("End task $i @ ${Thread.currentThread().name}")
        }
    }

    jobs.joinAll()
}

说明:

虽然我们启动了 10 个协程,但同一时间最多只有 3 个协程在执行。

不需要手动管理 Semaphore,limitedParallelism 自动挂起等待。

Channel(协程间直接通信)

Kotlin 协程中的 Channel,它是协程间直接通信(CSP 风格)的核心工具。

  • Channel 类似 线程安全的队列,用于 协程间传递数据。

  • 一个协程可以发送数据到 Channel,另一个协程可以接收数据。

  • 基于 CSP(Communicating Sequential Processes)模型,协程通过消息传递协作,而不是共享状态。

1. CSP 的核心思想

  • 进程(Process)

    • 每个进程是独立的、顺序执行的任务单元。

    • 进程内部可以做计算、处理数据,但不直接访问其他进程的状态。

  • 通信(Communication)

    • 进程之间不共享内存,而是通过 消息传递 进行交互。

    • 消息传递通常是 同步 或 挂起式:发送方和接收方在通信时协调完成。

  • 顺序执行(Sequential)

    • 每个进程内部是顺序执行的,但多个进程并行运行。

    • 并发是通过进程间的通信来协调,而不是共享变量和锁。

2. Kotlin 中的 CSP

Kotlin 协程的 Channel 就是 CSP 的体现:

  • 协程 = 进程

  • send() / receive() = 消息传递

  • 挂起点 = 进程在等待通信时暂停执行

特点:

  • 无共享状态:协程通过 Channel 传递数据,而不是访问同一变量。

  • 挂起式同步:发送方和接收方在必要时挂起,避免忙等待。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>(capacity = 5) // 带缓冲区

    // 生产者
    launch {
        for (i in 1..5) {
            println("Send $i")
            channel.send(i)
        }
        channel.close() // 关闭 Channel
    }

    // 消费者
    launch {
        delay(3000)	
        for (y in channel) { // 使用迭代器自动接收直到关闭
            println("Received $y")
            delay(300) // 模拟处理耗时
        }
    }
}


输出结果:

Send 1
Send 2
Send 3
Send 4
Send 5
Received 1
Received 2
Received 3
Received 4
Received 5

  • 生产者和消费者各自顺序执行自己的逻辑。

  • 通过 Channel 通信交换数据,而不共享任何变量。

  • 这种模式就是 CSP 风格:顺序进程 + 消息传递。

channel.close() 的作用

  • close() 并 不会丢掉已经发送到 Channel 的元素。

  • 它主要作用是 告诉接收方没有更多数据了,以及 阻止发送方继续发送。

换句话说:

  • 发送端 send() 之后的数据在缓冲区里仍然可被接收。

  • 接收端可以通过迭代(for (x in channel))或 receive() 继续取数据,直到缓冲区为空 且 Channel 被关闭。

  • 一旦关闭,后续 send() 会抛异常 ClosedSendChannelException。

3. Channel 类型

(1) 按角色分类(接口维度)

类型说明常用方法使用场景
SendChannel只能发送数据的接口send(), trySend()生产者(只写,不读)
ReceiveChannel只能接收数据的接口receive(), tryReceive(), consumeEach {}消费者(只读,不写)
Channel同时继承了 SendChannel<T>ReceiveChannel<T>既能 send() 也能 receive()通用场景
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

@OptIn(ExperimentalCoroutinesApi::class)
fun CoroutineScope.producer(): ReceiveChannel<Int> = produce {
    repeat(5) {
        send(it)
    }
}

fun main() = runBlocking {
    val numbers: ReceiveChannel<Int> = producer()
    for (x in numbers) {
        println(x)
    }
}

上面在 main 里只能 receive,不能 send,保证接口清晰。

(2) 按实现方式分类(容量/策略维度)

类型容量行为特征使用场景
Rendezvous Channel(默认)0发送方挂起直到接收方 receive()严格同步,一对一通信
Buffered ChannelN(固定大小)缓冲区满时 send() 挂起生产/消费速率不匹配
Unlimited Channelsend() 永不挂起,可能内存暴涨高吞吐量,允许堆积
Conflated Channel1(仅保留最新值)旧值会被覆盖,只保留最新状态流、UI 渲染
Actor ChannelN(由实现决定)封装状态 + 顺序处理消息并发安全的共享状态

(1) Rendezvous Channel(容量 = 0, 默认)

如果不使用缓冲, 那么发送端每send一个数据, 接收端就会收到一个数据, 如果没有接收端, 那么发送端就会一直挂起在send函数, 例如:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>() // 无缓冲区

    // 生产者
    launch {
        for (i in 1..5) {
            println("Send $i")
            channel.send(i) // 会挂起,直到有接收者
        }
        channel.close() // 关闭 Channel
    }

    // 消费者
    launch {
        delay(5000) // 延迟接收
        for (y in channel) { // 使用迭代器自动接收直到关闭
            println("Received $y")
            delay(300) // 模拟处理耗时
        }
    }
}

输出结果:

Send 1 // 打印这句后会挂起, 直到5秒后, 后面的日志才输出, 而且是每收到一个,才发送下一个
Received 1
Send 2
Received 2
Send 3
Received 3
Send 4
Received 4
Send 5
Received 5

(2) Buffered Channel(容量 = N)

有缓冲,发送可以先放进缓冲区,直到满了才挂起。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>(capacity = 2) // 缓冲区大小 2

    launch {
        repeat(3) {
            println("before Send $it")
            channel.send(it) // 第 0,1 放入缓冲区不会挂起,第 2 会挂起
            println("after Sent $it ✅")
        }
        
		channel.close() // 发送完后记得关闭channel
    }

    launch {
        delay(5000) // 接收晚点开始
        repeat(3) {
            val value = channel.receive()
            println("Received $value ✅")
        }
    }
}

输出结果:

before Send 0
after Sent 0 ✅
before Send 1
after Sent 1 ✅
before Send 2 // 打印这行的时候会挂起, 直到5秒后, 后面的内容才输出
Received 0 ✅
Received 1 ✅
Received 2 ✅
after Sent 2 ✅

(3) Unlimited Channel(容量无限大)

所有 send() 都不会挂起,但可能内存爆掉。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>(Channel.UNLIMITED)

    launch {
        repeat(5) {
            println("Send $it")
            channel.send(it) // 永远不会挂起
        }
        channel.close() // 记得关闭channel
    }

    launch {
        delay(2000) // 消费很慢
        for (x in channel) {
            println("Received $x")
        }
    }
}

输出结果:

Send 0
Send 1
Send 2
Send 3
Send 4 // 发送端全部事件都发送完, 并停留在此, 等待2秒后, 接收端消费后, 才开始打印下面的日志
Received 0
Received 1
Received 2
Received 3
Received 4

(4) Conflated Channel(容量 = 1, 只保留最新值)

丢弃旧消息,只保留最新。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>(Channel.CONFLATED)

    launch {
        repeat(5) {
            println("Send $it")
            channel.send(it) // 旧的值会被覆盖,只保留最新
        }
        channel.close()
    }

    launch {
        delay(2000) // 消费很慢
        for (x in channel) {
            println("Received $x") // 只会拿到最后一个
        }
    }
}

输出结果:

Send 0
Send 1
Send 2
Send 3
Send 4 // 发送端全部发送完后, 挂起在此, 等待2秒后, 接收端只会收到最后一次发送的值
Received 4

4.Channel 基本 API

分类方法功能
通用close(cause: Throwable?)关闭通道,之后不能再发送,接收方还能消费剩余数据
isClosedForSend是否已关闭发送端
isClosedForReceive是否已关闭接收端
发送端(SendChannel)send(element)挂起发送,缓冲满会挂起
trySend(element)(替代 offer尝试发送,立即返回 ChannelResult
接收端(ReceiveChannel)receive()挂起接收,缓冲空会挂起
tryReceive()(替代 poll尝试接收,立即返回 ChannelResult
receiveCatching()安全接收,即使关闭或异常也不会抛出,返回 ChannelResult
iterator() / for (x in channel)迭代消费数据,直到通道关闭
扩展函数consumeEach { ... }顺序消费所有元素,简化写法
consumeAsFlow() / receiveAsFlow()ReceiveChannel 转换为 Flow,方便与 Flow API 一起用
toList() / toSet()一次性收集所有元素到集合(需要通道关闭)

Channel 的挂起点行为

  • 发送挂起点:send() 当缓冲区满时挂起。

  • 接收挂起点:receive() 当缓冲区为空时挂起。

  • 取消安全:挂起时协程被取消会抛 CancellationException。

注意点

  • Channel 是协作式通信
    • 发送和接收都是挂起操作,协程需要通过挂起点合作。
  • 关闭 Channel
    • 生产者完成后要 close(),否则消费者可能无限挂起。
    • 关闭 Channel 不会丢失已经发送的消息
    • 消费者仍能接收到缓冲区里剩余的数据
    • 关闭 Channel 只会影响 发送端:禁止继续 send(),会抛异常
    • 接收端通过迭代或 receive 可以继续消费,直到缓冲区空 + Channel 关闭
  • 缓冲区选择
    • Rendezvous(无缓冲):严格同步,发送和接收必须匹配。
    • 缓冲型:允许一定程度的生产超前消费。
    • CONFLATED:只保留最新值,适合状态更新场景。
  • 性能
    • Channel 有一定开销,如果只做简单的计数器或状态传递,可以考虑 AtomicInteger 或 SharedFlow。

(1) receiveAsFlow

👉 扩展在 ReceiveChannel 上

  • 作用:把一个 ReceiveChannel 转换成一个 Flow。
  • 特点:是 非终结性 的,调用后你仍然可以继续使用这个 ReceiveChannel。
    • receiveAsFlow() 本质就是把 channel 的接收迭代器 转换成 Flow。
    • 它不会改变顺序,默认情况下你收到的就是 发送的顺序。
    • 多个收集器(collect)去收的时候,它们会 竞争同一个通道 的数据。数据被分摊,导致不是每个收集器都能拿到完整数据。
  • 使用场景:你想用 Flow 操作符(map、filter、collect 等)来消费通道里的数据。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*

fun main(): kotlin.Unit = runBlocking {
    val channel = Channel<Int>()
    launch {
        repeat(5) {
            delay(100)
            channel.send(it)
        }
        channel.close() // 关闭通道
    }

    val flow = channel.receiveAsFlow()

    launch { flow.collect { println("Collector #1 got $it") } }
    launch { flow.collect { println("Collector #2 got $it") } }
    launch { flow.collect { println("Collector #3 got $it") } }
}

输出结果:

Collector #1 got 0
Collector #2 got 1
Collector #3 got 2
Collector #1 got 3
Collector #2 got 4

(2) consumeAsFlow

👉 扩展在 ReceiveChannel 上

  • 作用:同样把通道转换成 Flow,但会在消费结束后自动取消(关闭)通道。
  • 特点:是 终结性 的,一旦调用,你就不能再用这个 ReceiveChannel。
    • 它会把 ReceiveChannel 消费掉(消费 = 只能用一次)。
    • 一旦这个 Flow 被收集过,通道就关闭/失效了, 再次collect就会抛出异常了。
  • 使用场景:你想一次性把通道内容流式处理掉,不再继续使用通道。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val channel = Channel<Int>()

    // 生产者
    launch {
        repeat(5) {
            channel.send(it)
        }
        channel.close()
    }

    // 使用 consumeAsFlow 消费,并自动关闭 channel
    channel.consumeAsFlow()
        .collect { println("Got $it") }

    // channel 已经被消费完,不能再使用
    println("Channel is closed: ${channel.isClosedForReceive}")
}

输出结果:

Got 0
Got 1
Got 2
Got 3
Got 4
Channel is closed: true

5. Actor 函数

在 Kotlin Coroutines 里,actor {} 确实是一个 函数(构建器),它不是像 Swift 那样的 actor 关键字。
actorkotlinx.coroutines.channels 包里的一个 扩展函数, 函数定义如下:

@ObsoleteCoroutinesApi
public fun <E> CoroutineScope.actor(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    block: suspend ActorScope<E>.() -> Unit
): SendChannel<E> {
    val newContext = newCoroutineContext(context)
    val channel = Channel<E>(capacity)
    val coroutine = if (start.isLazy)
        LazyActorCoroutine(newContext, channel, block) else
        ActorCoroutine(newContext, channel, active = true)
    if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
    coroutine.start(start, coroutine, block)
    return coroutine
}

它需要一个 CoroutineScope来启动一个协程,返回值是一个 SendChannel<E>,你可以往这个 channel 里发送消息(事件),而 Actor 内部会顺序处理这些消息。

查看源码可知通过ActorScope可以得到channel对象

@ObsoleteCoroutinesApi
public interface ActorScope<E> : CoroutineScope, ReceiveChannel<E> {
    /**
     * A reference to the mailbox channel that this coroutine [receives][receive] messages from.
     * It is provided for convenience, so that the code in the coroutine can refer
     * to the channel as `channel` as apposed to `this`.
     * All the [ReceiveChannel] functions on this interface delegate to
     * the channel instance returned by this function.
     */
    public val channel: Channel<E>
}

Actor = 协程 + Channel

  • 协程:在后台运行,循环处理消息
  • Channel:外部通过发送消息与 Actor 交互
  • ActorScope:提供对 channel 的访问
  • 返回的SendChannel必须手动调用 close() 来关闭通道,否则 actor 协程会一直挂起等待消息。

Kotlin 的 Actor 既可以用来做协程之间的通信,也可以用来保证并发安全。

1. 协程之间的通信

Actor 的底层就是 Channel,而 Channel 本身就是 协程之间的消息传递机制。

  • 一个协程(Producer)可以 send 消息到 Actor

  • Actor 协程会顺序 receive 消息并处理

所以从这个角度,Actor = 特殊的消息消费者,它让不同协程之间通过消息交互,而不用共享变量。

2. 保证并发安全

因为 Actor 协程内部是 串行处理消息 的,消息队列是 FIFO:

  • 即使多个协程同时调用 send(),消息会被排队

  • Actor 内部一次只处理一个消息

  • 不需要加锁,就能保证共享状态的安全访问

👉 所以 Actor 模型其实就是:让共享状态和逻辑都封装在一个协程里,外部只能通过消息和它交互。

示例:通信 + 并发安全

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

// 定义消息类型 (Actor 只能通过消息通信)
sealed class CounterMsg

// 递增消息
object Increment : CounterMsg()

// 查询当前值的消息,带一个 "回应信封"
// CompletableDeferred 相当于 "一个可写一次的 Future"
// 发送者可以等待它,Actor 收到后填入值
class GetValue(val response: CompletableDeferred<Int>) : CounterMsg()

@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg> {
    var counter = 0  // Actor 内部状态(只能 Actor 自己访问)

    // 消息循环,逐个顺序处理
    for (msg in channel) {
        when (msg) {
            is Increment -> counter++  // 收到递增指令
            is GetValue -> msg.response.complete(counter)
            // 收到查询指令,把结果写进 response
            // 这样调用方可以拿到 counter 的值
        }
    }
}

fun main(): kotlin.Unit = runBlocking {
    val counter: SendChannel<CounterMsg> = counterActor()

    // 并发发送 1000 个递增消息
    repeat(1000) {
        counter.send(Increment)
    }

    // 构造一个 CompletableDeferred 等待返回值
    val response = CompletableDeferred<Int>()

    // 发送 "查询消息",并把 response 附带过去
    counter.send(GetValue(response))

    // 挂起等待 Actor 回复(Actor 在 GetValue 时调用 complete)
    val value = response.await()

    println("Final count: $value") // 1000

    counter.close() // 使用完后, 必须手动关闭通道!!!
}

什么是Actor Isolation

所谓Isolation 就是隔离的意思, Actor函数内部的状态(state)只能被 Actor 自己的代码访问和修改,外部只能通过消息(send/await)与它交互。

  • 每个 Actor 就像一个“小隔间”或“小黑屋”,里面的状态对外是隔离的。
  • 外部协程、线程、任务 不能直接访问 Actor 内部的状态。
  • 外部要想改变 Actor 的状态,必须 发送消息,由 Actor 自己处理。
    👉 这样就避免了传统并发编程中的 共享内存冲突 和 加锁问题。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

sealed class CounterMsg
object Increment : CounterMsg()
class GetValue(val response: CompletableDeferred<Int>) : CounterMsg()

@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg> {
    var counter = 0 // Actor 内部状态(隔离)
    for (msg in channel) {
        when (msg) {
            is Increment -> counter++ // 只有 Actor 协程自己能改
            is GetValue -> msg.response.complete(counter) // 返回结果
        }
    }
}

fun main() = runBlocking {
    val counter = counterActor()

    // 并发修改,但不会有线程安全问题
    repeat(1000) { counter.send(Increment) }

    val response = CompletableDeferred<Int>()
    counter.send(GetValue(response))
    println("Final count: ${response.await()}") // 输出 1000

    counter.close()
}

6. producer 函数

producer 函数 其实和 actor 属于同一类东西,都是 kotlinx.coroutines 提供的 构建协程 + 通道的便捷函数。
它本质上是个 CoroutineScope 的扩展函数,返回一个 ReceiveChannel,用来表示“一个会不断生产数据的协程”。源码定义如下:

@ExperimentalCoroutinesApi
public fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = Channel.RENDEZVOUS,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> =
    produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block)
  • 返回值:ReceiveChannel(只能收数据的通道)。

  • 内部:会启动一个协程,把 block 里的逻辑放进去执行, 作为生产者实现数据的发送, 当生产者协程 执行结束时,通道会自动关闭。

  • 作用:专门用于生产者协程,负责往通道里发送数据, 然后得到返回值的地方去接收数据。

对比 actor

  • actor → 返回 SendChannel(别人给它发消息,它顺序处理 → 常用来做状态机/并发安全)。

  • produce → 返回 ReceiveChannel(它自己不断发数据,别人来消费 → 常用来做生产者)。

可以说:

  • actor 用于收消息并处理。

  • producer 用于发消息供别人消费。

(1) producer 构造生产者

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
    // 启动生产者
    val numbers: ReceiveChannel<Int> = produce {
        for (i in 1..5) {
            delay(500)
            println("Send $i")
            send(i)  // 向通道发送数据
        }
    }

    // 消费者
    delay(2000) // 延迟2秒后再消费
    for (x in numbers) {
        println("Received $x")
    }

    println("Done!")
}

输出结果:

Send 1 // 打印这行之后会挂起, 2秒后消费者开始接收, 每收到一个, 才会发送下一个
Received 1
Send 2
Received 2
Send 3
Received 3
Send 4
Received 4
Send 5
Received 5
Done!

(2) producer 结合 actor 使用

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

@OptIn(ExperimentalCoroutinesApi::class, ObsoleteCoroutinesApi::class)
fun main(): kotlin.Unit = runBlocking {
    // 生产者:产生数字
    val producer: ReceiveChannel<Int> = produce {
        for (i in 1..10) {
            send(i)
        }
    }

    // 消费者 Actor:计算平方
    val squareActor: SendChannel<Int> = actor {
        for (num in channel) {
            println("Square of $num = ${num * num}")
        }
    }

    // 把生产者的数据发给 Actor
    for (num in producer) {
        squareActor.send(num)
    }

    producer.cancel()
    squareActor.close() // 关闭发送端
}

输出结果:

Square of 1 = 1
Square of 2 = 4
Square of 3 = 9
Square of 4 = 16
Square of 5 = 25
Square of 6 = 36
Square of 7 = 49
Square of 8 = 64
Square of 9 = 81
Square of 10 = 100

总结:

  • producer {} → 返回 ReceiveChannel,用于“生产者协程”。
  • actor {} → 返回 SendChannel,用于“消息处理协程”。
  • 它们都是 CoroutineScope 的扩展函数,必须在一个 scope 内使用。

👉 所以 produce 和 actor 是 对 Channel 的模式化封装,目的就是:

  • produce = 简化生产者协程 + 自动关闭。

  • actor = 简化消费者协程 + 保证并发安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值