`launch` 函数的第二个参数是 `CoroutineStart`,用于设置协程的启动模式。在了解各启动模式前,需要先区分两个容易混淆的概念:**立即调度**和**立即执行**。
launch 函数签名
`launch` 的函数签名大致如下:
```kotlin
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
```
其中,`start` 参数决定协程以什么方式启动。
立即调度与立即执行
立即调度
立即调度是指:
> 调用 `launch` 后,协程任务立刻被提交给对应的调度器,等待线程执行。
任务被调度不代表协程体会在调用点立即运行。
```kotlin
fun main() = runBlocking {
println("A")
launch {
println("B")
}
println("C")
}
```
通常输出:
```text
A
C
B
```
执行到 `launch` 时,子协程已经进入待调度状态,但当前线程仍在执行 `runBlocking` 中的后续代码,因此先打印 `C`。调度器随后执行子协程并打印 `B`。
> 输出顺序与具体调度器和运行环境有关。这个例子用于说明"已提交调度"不等于"协程体已经执行"。
立即执行
立即执行是指:
> 调用协程构建器时,直接在当前调用线程中开始执行协程体,直到遇到第一个真正挂起点。
```kotlin
fun main() = runBlocking {
println("A")
launch(start = CoroutineStart.UNDISPATCHED) {
println("B")
delay(100)
println("D")
}
println("C")
}
```
通常输出:
```text
A
B
C
D
```
`UNDISPATCHED` 会立即在当前线程执行 `println("B")`。执行到 `delay(100)` 并真正挂起后,`launch` 调用才返回,外层代码继续打印 `C`。子协程恢复后再打印 `D`。
CoroutineStart 的四种启动模式
1. DEFAULT:立即调度
```kotlin
launch(start = CoroutineStart.DEFAULT) {
println("执行任务")
}
```
特点:
-
调用 `launch` 后立即将任务提交给调度器。
-
不保证协程体立刻开始执行。
-
协程在真正开始执行前可以被取消。
-
这是默认模式,也是最常用的模式。
示例:
```kotlin
fun main() = runBlocking {
val job = launch(start = CoroutineStart.DEFAULT) {
println("任务开始")
}
job.cancel()
}
```
如果取消操作发生在协程体开始执行之前,`任务开始` 可能不会输出。
2. LAZY:延迟启动
```kotlin
val job = launch(start = CoroutineStart.LAZY) {
println("任务开始")
}
```
创建协程时不会立即启动。以下操作可以触发启动:
```kotlin
job.start()
job.join()
```
示例:
```kotlin
fun main() = runBlocking {
println("A")
val job = launch(start = CoroutineStart.LAZY) {
println("B")
}
println("C")
job.join()
println("D")
}
```
输出:
```text
A
C
B
D
```
注意事项:如果创建了一个 `LAZY` 子协程,却既不启动也不取消它,父协程结束时仍需等待该子协程,可能导致程序一直等待。
3. ATOMIC:原子启动
```kotlin
launch(start = CoroutineStart.ATOMIC) {
println("任务开始")
delay(100)
println("任务结束")
}
```
特点:
-
创建后立即调度。
-
从创建到开始执行之前,取消操作不能阻止协程进入协程体。
-
协程开始执行并到达首个可取消挂起点后,恢复正常的协作式取消行为。
-
适用于必须保证初始化逻辑至少开始执行的特殊场景。
示例:
```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
val job = launch(start = CoroutineStart.ATOMIC) {
println("一定有机会执行")
delay(100)
println("可能不会执行")
}
job.cancelAndJoin()
}
```
通常会输出:
```text
一定有机会执行
```
调用 `cancelAndJoin()` 时,取消操作无法阻止协程进入协程体,因此第一行有机会执行。当代码到达 `delay()` 等可取消挂起点时,会检测到取消状态,后续代码通常不再执行。
> `ATOMIC` 属于特殊启动模式,并涉及实验性 API,业务代码中应谨慎使用。
4. UNDISPATCHED:立即执行
```kotlin
launch(start = CoroutineStart.UNDISPATCHED) {
println("立即执行")
delay(100)
println("恢复执行")
}
```
特点:
-
不先经过调度器,直接在当前调用线程执行协程体。
-
一直执行到第一个真正挂起点。
-
挂起后再次恢复时,按照协程上下文中的调度器运行。
-
如果调用的挂起函数没有真正挂起,协程会继续同步执行。
示例:
```kotlin
fun main() = runBlocking {
println("外层线程:${Thread.currentThread().name}")
launch(
context = Dispatchers.Default,
start = CoroutineStart.UNDISPATCHED
) {
println("挂起前:${Thread.currentThread().name}")
delay(100)
println("恢复后:${Thread.currentThread().name}")
}
}
```
可能输出:
```text
外层线程:main
挂起前:main
恢复后:DefaultDispatcher-worker-1
```
这说明 `UNDISPATCHED` 模式下,协程体在首次挂起前运行于调用线程;挂起后恢复时,才使用协程上下文中的 `Dispatchers.Default`。
启动模式对比
| 启动模式 | 创建后是否启动 | 开始方式 | 启动前的取消行为 | 典型用途 |
| --- | --- | --- | --- | --- |
| `DEFAULT` | 是 | 立即提交调度 | 可以在执行前取消 | 默认业务场景 |
| `LAZY` | 否 | 调用 `start()`、`join()` 等操作后调度 | 启动前可以取消 | 按需启动 |
| `ATOMIC` | 是 | 原子调度启动 | 取消不能阻止协程进入协程体 | 必须保证开始执行的特殊任务 |
| `UNDISPATCHED` | 是 | 在当前线程立即执行到首次挂起 | 初始代码会直接执行 | 同步初始化、减少首次调度 |
核心理解
> `DEFAULT` 是立即把任务提交给调度器,但不保证协程体马上运行;`UNDISPATCHED` 才是在调用点直接执行协程体,直到第一次真正挂起。
可以简单记忆为:
-
`DEFAULT`:现在调度。
-
`LAZY`:以后启动。
-
`ATOMIC`:保证启动。
-
`UNDISPATCHED`:现在执行。