本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、协程的启动模式:CoroutineStart
协程启动时可以指定启动模式,不同的启动模式在协程调度和取消时有不同的表现。根据不同的使用场景选择合适的启动模式,可以让并发任务更加灵活。
launch(start = CoroutineStart.DEFAULT) {
}
协程的启动模式有以下几个选项:
- DEFAULT: 协程创建后,立即开始调度,协程在调度前可以被取消。
- ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。
- LAZY:协程创建后,不会被调度,只有协程被需要时,包括主动调用协程的 start()、join()、await() 等函数时才会开始调度,协程在调度前可以被取消。
- UNDISPATCHED: 协程创建后立即在当前函数调用栈中执行,直到遇到第一个挂起点后才会切换到协程上下文中执行。
二、默认启动模式:DEFAULT
先看 DEFAULT 启动模式:
runBlocking {
val job = launch(start = CoroutineStart.DEFAULT) {
Log.d("~~~", "start")
delay(5000)
Log.d("~~~", "done")
}
job.cancel()
}
运行这段程序会发现没有任何输出,因为协程在调度前就被取消了,没有来得及执行。
有的读者可能会疑惑,DEFAULT 启动模式不是在协程创建后,立即开始调度吗?为什么开始调度了却没有执行呢?
这是因为开始调度和开始执行是不一样的,开始调度到真正执行之前还有一些逻辑。比如多个协程同时开始调度时,各个协程就会抢占 CPU,先抢占到的先执行,没有抢占到的协程将会等待 CPU 空出来,这种处于等待状态的协程就属于已经开始调度,但还没有得到执行的协程。
接下来我们对比一下同样的代码在其他几种启动模式下的表现,可以更加理解这段话的含义。
三、原子启动模式:ATOMIC
我们用上文中同样的代码,只把启动模式改成 ATOMIC,结果如何呢:
runBlocking {
val job = launch(start = CoroutineStart.ATOMIC) {
Log.d("~~~", "start")
delay(5000)
Log.d("~~~", "done")
}
job.cancel()
}
运行这段程序会发现输出了 start,表明协程在遇到 delay() 这个挂起点时才被 cancel。
ATOMIC 译为「原子的」。在这种启动模式下,协程创建后也会立即开始调度。但在协程开始执行前,不会响应 cancel()。上文说到,协程调度后还需要一段时间才会执行,使用 ATOMIC 启动模式,可以保证协程一定能得到执行,而不会在执行前就被取消,这一点和 DEFAULT 是不一样的,DEFAULT 启动模式下,执行前协程也会响应取消。
那么这里为什么能够输出 start 呢?如果协程刚得到执行,就被 cancel() 了,那就不应该有任何输出才对。
这是因为协程取消时,并不是运行到任何地方都会直接取消的,必须要遇到一个挂起点才能取消,本例中,delay() 函数是一个挂起函数,所以这里就是一个挂起点,协程运行到这里的时候才响应了取消命令。
四、懒启动模式:LAZY
相信大家都听说过应用数据的懒加载、单例模式的懒汉式等概念。这个概念将代码中的某些对象用到时才初始化的特点,比喻成不到万不得已不会行动的懒人。
顾名思义,懒启动模式也是一样,如果协程的启动模式是懒启动,那么协程初始化时就不会开始调度。必须手动调用 start()、await() 或 join() 后,协程才会开始调度。
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
Log.d("~~~", "start")
delay(5000)
Log.d("~~~", "done")
}
job.start()
}
在这段代码中,如果不调用 job.start(),将不会有任何输出。只有调用了 job.start() 后,程序才能正常执行。
五、不分配启动模式:UNDISPATCHED
最后一种启动模式是不分配启动模式,当使用这种启动模式时,协程会立即开始执行,注意不是开始调度,而是直接开始执行。
读者可能又有疑问了,协程都没有调度,怎么能直接执行呢?
答案是协程中的代码块会立即在当前函数调用栈中执行,直到遇到一个挂起点,才会切换到协程的上下文中去调度、执行。
GlobalScope.launch(Dispatchers.Main) {
launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
Log.d("~~~", "start ${Thread.currentThread().name}")
delay(5000)
Log.d("~~~", "done ${Thread.currentThread().name}")
}
}
运行这段程序,输出如下:
~~~: start main
~~~: done DefaultDispatcher-worker-1
可以看出,在遇到挂起点前,也就是 delay() 函数执行前,协程中的代码块是在主线程中执行的,并没有切换到 Dispatchers.IO 调度器中执行。直到运行到 delay() 函数时,协程才会切换到 Dispatchers.IO 调度器中去执行。
这就是上文提到的,当协程启动模式是 UNDISPATCHED 的时候,协程创建后会立即在当前函数调用栈中执行,直到遇到第一个挂起点后才会切换到协程上下文中执行。
六、小结
本文介绍了协程的四种启动模式。
- 默认的启动模式是 DEFAULT,这种启动模式的表现比较符合正常的逻辑。创建后就调度,调度前可取消。
- ATOMIC 启动模式可以保证协程一定得到执行,在执行到挂起点前不响应取消。
- LAZY 启动模式创建后不调度,需要时才会调度,调度前可取消。
- UNDISPATCHED 启动模式会让协程立即在当前函数调用栈中执行,直到遇到挂起点后,才会切换到协程调度器中开始调度。
注:本文中对四种启动模式名字的中文翻译纯属笔者胡诌,笔者没有找到官方的翻译,建议大家直接使用英文名交流。