关键词:Kotlin 协程 启动模式安全
如今你已经知道协程大概是怎么回事了,也应该想要本身尝试一把了吧。本文将为你们详细介绍协程的几种启动模式之间的不一样,固然,我不打算如今就开始深刻源码剖析原理,你们只须要记住这些规则就能很好的使用协程了。bash
我相信如今接触 Kotlin 的开发者绝大多数都有 Java 基础,咱们刚开始学习 Thread 的时候,必定都是这样干的:多线程
val thread = object : Thread(){
override fun run() {
super.run()
//do what you want to do.
}
}
thread.start()
复制代码
确定有人忘了调用 start
,还特别纳闷为啥我开的线程不启动呢。说实话,这个线程的 start
的设计实际上是很奇怪的,不过我理解设计者们,毕竟当年还有 stop
能够用,结果他们很快发现设计 stop
就是一个错误,由于不安全而在 JDK 1.1 就废弃,称得上是最短命的 API 了吧。异步
既然
stop
是错误,那么老是让初学者丢掉的start
是否是也是一个错误呢?ide
哈,有点儿跑题了。咱们今天主要说 Kotlin。Kotlin 的设计者就颇有想法,他们为线程提供了一个便捷的方法:函数
val myThread = thread {
//do what you want
}
复制代码
这个 thread
方法有个参数 start
默认为 true
,换句话说,这样创造出来的线程默认就是启动的,除非你实在不想让它立刻投入工做:post
val myThread = thread(start = false) {
//do what you want
}
//later on ...
myThread.start()
复制代码
这样看上去天然多了。接口设计就应该让默认值知足 80% 的需求嘛。学习
说了这么多线程,缘由嘛,毕竟你们对它是最熟悉的。协程的 API 设计其实也与之一脉相承,咱们来看一段最简单的启动协程的方式:spa
GlobalScope.launch {
//do what you want
}
复制代码
那么这段代码会怎么执行呢?咱们说过,启动协程须要三样东西,分别是 上下文、启动模式、协程体,协程体 就比如 Thread.run
当中的代码,自没必要说。.net
本文将为你们详细介绍 启动模式。在 Kotlin 协程当中,启动模式是一个枚举:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
复制代码
模式 | 功能 |
---|---|
DEFAULT | 当即执行协程体 |
ATOMIC | 当即执行协程体,但在开始运行以前没法取消 |
UNDISPATCHED | 当即在当前线程执行协程体,直到第一个 suspend 调用 |
LAZY | 只有在须要的状况下运行 |
四个启动模式当中咱们最经常使用的实际上是 DEFAULT
和 LAZY
。
DEFAULT
是饿汉式启动,launch
调用后,会当即进入待调度状态,一旦调度器 OK 就能够开始执行。咱们来看个简单的例子:
suspend fun main() {
log(1)
val job = GlobalScope.launch {
log(2)
}
log(3)
job.join()
log(4)
}
复制代码
说明: main 函数 支持 suspend 是从 Kotlin 1.3 开始的。另外,main 函数省略参数也是 Kotlin 1.3 的特性。后面的示例没有特别说明都是直接运行在 suspend main 函数当中。
这段程序采用默认的启动模式,因为咱们也没有指定调度器,所以调度器也是默认的,在 JVM 上,默认调度器的实现与其余语言的实现相似,它在后台专门会有一些线程处理异步任务,因此上述程序的运行结果多是:
19:51:08:160 [main] 1
19:51:08:603 [main] 3
19:51:08:606 [DefaultDispatcher-worker-1] 2
19:51:08:624 [main] 4
复制代码
也多是:
20:19:06:367 [main] 1
20:19:06:541 [DefaultDispatcher-worker-1] 2
20:19:06:550 [main] 3
20:19:06:551 [main] 4
复制代码
这取决于 CPU 对于当前线程与后台线程的调度顺序,不过不要担忧,很快你就会发现这个例子当中 2 和 3 的输出顺序其实并无那么重要。
JVM 上默认调度器的实现也许你已经猜到,没错,就是开了一个线程池,但区区几个线程足以调度成千上万个协程,并且每个协程都有本身的调用栈,这与纯粹的开线程池去执行异步任务有本质的区别。
固然,咱们说 Kotlin 是一门跨平台的语言,所以上述代码还能够运行在 JavaScript 环境中,例如 Nodejs。在 Nodejs 中,Kotlin 协程的默认调度器则并无实现线程的切换,输出结果也会略有不一样,这样彷佛更符合 JavaScript 的执行逻辑。
更多调度器的话题,咱们后续还会进一步讨论。
LAZY
是懒汉式启动,launch
后并不会有任何调度行为,协程体也天然不会进入执行状态,直到咱们须要它执行的时候。这其实就有点儿费解了,什么叫咱们须要它执行的时候呢?就是须要它的运行结果的时候, launch
调用后会返回一个 Job
实例,对于这种状况,咱们能够:
Job.start
,主动触发协程的调度执行Job.join
,隐式的触发协程的调度执行因此这个所谓的”须要“,实际上是一个颇有趣的措辞,后面你还会看到咱们也能够经过 await
来表达对 Deferred
的须要。这个行为与 Thread.join
不同,后者若是没有启动的话,调用 join
不会有任何做用。
log(1)
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
job.start()
log(4)
复制代码
基于此,对于上面的示例,输出的结果多是:
14:56:28:374 [main] 1
14:56:28:493 [main] 3
14:56:28:511 [main] 4
14:56:28:516 [DefaultDispatcher-worker-1] 2
复制代码
固然若是你运气够好,也可能出现 2 比 4 在前面的状况。而对于 join
,
...
log(3)
job.join()
log(4)
复制代码
由于要等待协程执行完毕,所以输出的结果必定是:
14:47:45:963 [main] 1
14:47:46:054 [main] 3
14:47:46:069 [DefaultDispatcher-worker-1] 2
14:47:46:090 [main] 4
复制代码
ATOMIC
只有涉及 cancel 的时候才有意义,cancel 自己也是一个值得详细讨论的话题,在这里咱们就简单认为 cancel 后协程会被取消掉,也就是再也不执行了。那么调用 cancel 的时机不一样,结果也是有差别的,例如协程调度以前、开始调度但还没有执行、已经开始执行、执行完毕等等。
为了搞清楚它与 DEFAULT
的区别,咱们来看一段例子:
log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
log(2)
}
job.cancel()
log(3)
复制代码
咱们建立了协程后当即 cancel,但因为是 ATOMIC
模式,所以协程必定会被调度,所以 一、二、3 必定都会输出,只是 2 和 3 的顺序就难说了。
20:42:42:783 [main] 1
20:42:42:879 [main] 3
20:42:42:879 [DefaultDispatcher-worker-1] 2
复制代码
对应的,若是是 DEFAULT
模式,在第一次调度该协程时若是 cancel 就已经调用,那么协程就会直接被 cancel 而不会有任何调用,固然也有可能协程开始时还没有被 cancel,那么它就能够正常启动了。因此前面的例子若是改用 DEFAULT
模式,那么 2 有可能会输出,也可能不会。
须要注意的是,cancel 调用必定会将该 job 的状态置为 cancelling,只不过ATOMIC
模式的协程在启动时无视了这一状态。为了证实这一点,咱们可让例子稍微复杂一些:
log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
log(2)
delay(1000)
log(3)
}
job.cancel()
log(4)
job.join()
复制代码
咱们在 2 和 3 之间加了一个 delay
,delay
会使得协程体的执行被挂起,1000ms 以后再次调度后面的部分,所以 3 会在 2 执行以后 1000ms 时输出。对于 ATOMIC
模式,咱们已经讨论过它必定会被启动,实际上在遇到第一个挂起点以前,它的执行是不会中止的,而 delay
是一个 suspend 函数,这时咱们的协程迎来了本身的第一个挂起点,刚好 delay
是支持 cancel 的,所以后面的 3 将不会被打印。
咱们使用线程的时候,想要让线程里面的任务中止执行也会面临相似的问题,但遗憾的是线程中看上去与 cancel 相近的 stop 接口已经被废弃,由于存在一些安全的问题。不过随着咱们不断地深刻探讨,你就会发现协程的 cancel 某种意义上更像线程的 interrupt。
有了前面的基础,UNDISPATCHED
就很容易理解了。协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点,这听起来有点儿像前面的 ATOMIC
,不一样之处在于 UNDISPATCHED
不通过任何调度器即开始执行协程体。固然遇到挂起点以后的执行就取决于挂起点自己的逻辑以及上下文当中的调度器了。
log(1)
val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
log(2)
delay(100)
log(3)
}
log(4)
job.join()
log(5)
复制代码
咱们仍是以这样一个例子来认识下 UNDISPATCHED
模式,按照咱们前面的讨论,协程启动后会当即在当前线程执行,所以 一、2 会连续在同一线程中执行,delay
是挂起点,所以 3 会等 100ms 后再次调度,这时候 4 执行,join
要求等待协程执行完,所以等 3 输出后再执行 5。如下是运行结果:
22:00:31:693 [main] 1
22:00:31:782 [main @coroutine#1] 2
22:00:31:800 [main] 4
22:00:31:914 [DefaultDispatcher-worker-1 @coroutine#1] 3
22:00:31:916 [DefaultDispatcher-worker-1 @coroutine#1] 5
复制代码
方括号当中是线程名,咱们发现协程执行时会修改线程名来让本身显得很有存在感。运行结果看上去还有一个细节可能会让人困惑,
join
以后的 5 的线程与 3 同样,这是为何?咱们在前面提到咱们的示例都运行在 suspend main 函数当中,因此 suspend main 函数会帮咱们直接启动一个协程,而咱们示例的协程都是它的子协程,因此这里 5 的调度取决于这个最外层的协程的调度规则了。关于协程的调度,咱们后面再聊。
本文经过一些例子来给你们逐步揭开协程的面纱。相信你们读完对于协程的执行机制有了一个大概的认识,同时对于协程的调度这个话题想必也很是好奇或者感到困惑,这是正常的——由于咱们尚未讲嘛,放心,调度器的内容已经安排了 : )。
log
函数的定义:
val dateFormat = SimpleDateFormat("HH:mm:ss:SSS")
val now = {
dateFormat.format(Date(System.currentTimeMillis()))
}
fun log(msg: Any?) = println("${now()} [${Thread.currentThread().name}] $msg")
复制代码
欢迎关注 Kotlin 中文社区!
中文官网:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公众号:Kotlin
知乎专栏:Kotlin
CSDN:Kotlin中文社区
掘金:Kotlin中文社区
简书:Kotlin中文社区