Android协程(Coroutines)系列-深入理解CoroutineStart启动模式

4,160 阅读4分钟

小知识,大挑战!本文正在参与“   程序员必备小知识   ”创作活动

本文同时参与 「掘力星计划」   ,赢取创作大礼包,挑战创作激励金

📚 如果您是 Android 平台上协程的初学者,请查阅上一篇文章: Android协程(Coroutines)系列-入门

🍈 CoroutineStart简介

其实Android协程(Coroutines)系列-入门文章中的GlobalScope.launch中的构造方法中需要传入上下文context,启动模式start,协程体block,今日讲解一下协程的CoroutineStart启动模式.如果不传,默认是 CoroutineStart.DEFAULT,launch()源码如下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

在 Kotlin 协程当中,CoroutineStart启动模式是一个枚举:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}
模式功能
DEFAULT立即进入待调度状态
LAZY只有需要(start/join/await)时才开始调度
ATOMIC和DEFAULT类似,且在第一个挂起点前不能被取消
UNDISPATCHED立即在当前线程执行协程体,直到遇到第一个挂起点(后面取决于调度器)

🍈 DEFAULT

四个启动模式当中我们最常用的其实是 DEFAULT 和 LAZY

DEFAULT 是饿汉式启动,launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行。例子:

suspend fun main() {
    println(1)

    val job = GlobalScope.launch {
        println(2)
    }

    println(3)

    job.join()//等待协程执行完毕

    println(4)
}

打印结果

1
3
2
4

上面的代码采用的是默认的启动模式,并且也没有指定调度器,所以调度器也是默认的。在JVM上默认调度器的实现是开一个线程池。

但区区几个线程足以调度成千上万个协程,而且每一个协程都有自己的调用栈,这与纯粹的开线程池去执行异步任务有本质的区别。

🍈LAZY

LAZY 是懒汉式启动,launch 后并不会有任何调度行为,协程体也自然不会进入执行状态,直到我们需要它执行的时候。

  • 调用 Job.start,主动触发协程的调度执行
  • 调用 Job.join,隐式的触发协程的调度执行
  • 调用 async.await()
suspend fun main() {
    println(1)

    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        println(2)
    }

    println(3)

    job.start()//协程开始

    println(4)

    //这里的sleep只是保持进程存活, 目的是为了等待协程执行完
    Thread.sleep(5000L)
}

打印结果

1
3
4
2

🍈ATOMIC

ATOMICDEFAULT类似,且在第一个挂起点前不能被取消

ATOMIC 只有涉及 cancel(协程能够被取消,是需要一定条件的) 的时候才有意义。那么调用 cancel 的时机不同,结果也是有差异的,例如协程调度之前、开始调度但尚未执行、已经开始执行、执行完毕等等。

🔺 注意: kotlinx.coroutines包下的所有挂起函数都是可取消的。这些挂起函数会检查协程的取消状态,当取消时就会抛出CancellationException异常

@ExperimentalCoroutinesApi
suspend fun main() {
    println(1)

    val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
        println("suspend挂起之前")
        delay(2000)
        println(2)
    }

    println(3)

    job.cancel()//协程取消

    println(4)
}

打印结果

1
3
suspend挂起之前
4

对于 ATOMIC 模式,我们已经讨论过它一定会被启动,实际上在遇到第一个挂起点之前,它的执行是不会停止的,而 delay 是一个 suspend 函数,这时我们的协程迎来了自己的第一个挂起点,恰好 delay 是支持 cancel 的,因此后面的 2 将不会被打印。

🍈UNDISPATCHED

协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点,这听起来有点儿像前面的 ATOMIC,不同之处在于 UNDISPATCHED 不经过任何调度器即开始执行协程体。当然遇到挂起点之后的执行就取决于挂起点本身的逻辑以及上下文当中的调度器了。

@ExperimentalCoroutinesApi
suspend fun main() {
    println(1)

    val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
        println("suspend挂起之前")
        delay(2000)
        println(2)
    }

    println(3)

    job.join()

    println(4)
}

打印结果

1
suspend挂起之前
3

2
4

协程启动后会立即在当前线程执行,因此 1、"suspend挂起之前" 会连续在同一线程(main线程)中执行,delay 是挂起点,因此 2 会等 2000ms 后再次调度,这时候 3 执行,join 要求等待协程执行完,因此等 2 输出后再执行 4。