笔者最近一直在研究协程。至于协程是什么。不在本文的介绍范围。之后会专门说一下。协程与线程的区别。这篇文章主要还是介绍一下协程的启动模式。对,你没听错。协程也是有启动模式的
协程的构造方法
先看一下协程的构造方法,其第二个参数就是启动模式。而且还给了一个默认的模式 DEFAULT
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
}
启动模式类型
点开CoroutineStart启动模式的枚举类型如下 默认就是DEFAULT
public enum class CoroutineStart {
//协程创建后。立即开始调度。在调度前如果协程被取消。直接进入取消响应的状态
DEFAULT,
//当我们需要他执行的时候才会执行,不然就不会执行。
LAZY,
//立即开始调度,协程之前到第一个挂起点之前是不响应取消的
ATOMIC,
//创建协程后立即在当前函数的调用栈中执行。直到遇到第一个挂起点为止。
UNDISPATCHED
}
要搞懂启动模式之前,需要有几个概念需要理解
- 调度不代表执行。
这里的调度,说的其实是第一个参数。上下文
CoroutineContext。暂时可以理解成一个队列,协程任务被添加到这个队列。不代表执行了。 - 调度到执行之间是有时间的
- 协程可以被取消
DEFAULT
查看DEFAULT。的官网介绍
协程创建后。立即开始调度。在调度前如果协程被取消。直接进入取消响应的状态
先发上一个笔者画的图。稍后来解释一下
协程创建后。立即开始调度这句话的理解。比较有意思。协程任务被添加入调度器,也就是之前说的队列。不需要去start() 或者 join()任务自行就会触发。下面的测试代码。是否开始start()对于测试结果来说是没有任务影响的。
在调度前如果协程被取消。直接进入取消响应的状态,这个更加有意思。首先创建一个协程任务并且添加到队列中。任务就会自行触发。cancel 的时间就很难把握,是在任务执行前呢,还是在执行后呢,即使创建协程以后。立马cancel,也有可能调度器已经在执行协程的代码块了。
在解释一下下面的测试代码,笔者为了获取多次结果。跑了100次,delay(100)是一个挂起函数,之后会说明。sleep(20) 是在执行方法的线程上。阻塞20毫秒。作用就是为了保证任务一定被添加到调度器中。
private fun todoModuleDefault(
start: CoroutineStart = CoroutineStart.DEFAULT,
isStart: Boolean = false
) {
for (i in 1..100) {
val buffer = StringBuffer()
buffer.append("start")
val job = GlobalScope.launch(start = start) {
buffer.append("->执行协程")
delay(100)
buffer.append("->协程执行完成")
}
buffer.append("->启动协程")
//是否start 不会影响结果
if (isStart) {
job.start()
}
//确保一定执行了 start()
sleep(20)
buffer.append("->取消协程")
job.cancel()
buffer.append("->end")
println("第${i}次 $buffer")
}
}
运行结果如下。启动协程 和 执行协程的先后顺序会发生变化。如同我们上面说的一样。协程任务添加到调度器之后。会立马执行。才不会管你有没有 start() 而谁先谁后完全取决于当时的调度器状态。无法控制。
cancel()方法,在20毫秒之后执行。而此时的协程已经被挂起了。delay(100)。这时候cancel(),就没有在打印 协程执行完成。也证实了协程是可以被取消的。
第13次 start->启动协程->执行协程->取消协程->end
第17次 start->执行协程->启动协程->取消协程->end
LAZY
当我们需要他执行的时候才会执行,不然就不会执行。如果调度前就被取消。那么直接进入异常结束状态
所谓的需要他执行的时候才会执行,就是执行了 start()或者 join() 才会启动。将上面的测试代码修改成 LAZY 模式。此时isStart变量改成true执行结果如下
start->启动协程->执行协程->取消协程->end
改成false 不执行start()的测试结果如下
start->启动协程->取消协程->end
很明显。是否执行协程。完全取决于我是否start()
如果调度前就被取消。那么直接进入异常结束状态,就是说将协程任务添加到调度器中。等待被执行。此时将去取消。那么协程是不会在执行的。
🌰举个例子
看一下下面的示例。故意cancel()之后再去start()
private fun todoModuleLazyCancel(
start: CoroutineStart = CoroutineStart.LAZY
) {
for (i in 1..100) {
val buffer = StringBuffer()
buffer.append("start")
val job = GlobalScope.launch(start = start) {
buffer.append("->执行协程")
delay(100)
buffer.append("->协程执行完成")
}
buffer.append("->取消协程")
job.cancel()
//确保一定执行了 cancel()
sleep(20)
buffer.append("->启动协程")
job.start()
buffer.append("->end")
println("第${i}次 $buffer")
}
}
执行结果,协程不会在被执行了。
start->取消协程->启动协程->end
ATOMIC
立即开始调度,协程之前到第一个
挂起点之前是不响应取消的
什么叫做挂起点呢,就是执行挂起函数的地方,也就是执行suspend修饰过的方法函数。在示例中 delay 就是一个挂起函数
他和DEFAULT有点像。都是直接开始调度。不用去start()或者join()。
不同点就是在到第一个挂起点之前是不响应取消的,而DEFAULT就没这个要求,随时可以取消。
🌰举个例子
将模式改成ATOMIC,是否start()不会影响结果
private fun todoModuleAtomic(
start: CoroutineStart = CoroutineStart.ATOMIC,
isStart: Boolean = false
) {
for (i in 1..100) {
val buffer = StringBuffer()
buffer.append("start")
val job = GlobalScope.launch(start = start) {
buffer.append("->执行协程")
delay(100)
buffer.append("->协程执行完成")
}
buffer.append("->启动协程")
//是否start 不会影响结果
if (isStart) {
job.start()
}
//确保一定执行了 start()
sleep(20)
buffer.append("->取消协程")
job.cancel()
buffer.append("->end")
println("第${i}次 $buffer")
}
}
执行结果如下。因为在第一个挂起点之前不响应取消cancel()所以也就决定了。协程一定会执行
2021-01-19 20:50:58.780 I: 第3次 start->启动协程->执行协程->取消协程->end
2021-01-19 20:50:58.982 I: 第4次 start->执行协程->启动协程->取消协程->end
UNDISPATCHED
创建协程后立即在当前函数的调用栈中执行。直到遇到第一个
挂起点为止。
乍一看感觉和ATOMIC很像。注意了。这里是在当前函数的调用栈,啥意思呢?就是在你执行协程的线程。例如下面的示例。我实在主线程上执行的。那么在第一个挂起点之前。也就是 delay(20)之前。也是在主线程。而ATOMIC就是在调度器所创建的线程。
private fun todoModuleUnDispatched(
start: CoroutineStart = CoroutineStart.UNDISPATCHED,
isStart: Boolean = false
) {
for (i in 1..100) {
val buffer = StringBuffer()
buffer.append("start")
val job = GlobalScope.launch(start = start) {
buffer.append("->执行协程A")
buffer.append("[${Thread.currentThread().name}]")
delay(20)
buffer.append("->执行协程B")
buffer.append("[${Thread.currentThread().name}]")
delay(100)
buffer.append("->协程执行完成")
}
buffer.append("->启动协程")
//是否start 不会影响结果
if (isStart) {
job.start()
}
//确保一定执行了 start()
sleep(50)
buffer.append("->取消协程")
job.cancel()
buffer.append("->end")
println("第${i}次 $buffer")
}
}
UNDISPATCHED模式下的日志。在主线程
start->执行协程A[main]->启动协程->执行协程B[DefaultDispatcher-worker-1]->取消协程->end
LAZY模式下的日志,在后台线程。
start->启动协程->执行协程A[DefaultDispatcher-worker-2]->执行协程B[DefaultDispatcher-worker-1]->取消协程->end
start->启动协程->执行协程A[DefaultDispatcher-worker-1]->执行协程B[DefaultDispatcher-worker-1]->取消协程->end
还有一点。因为UNDISPATCHED创建协程后立即在当前函数的调用栈中执行。所以他的协程也一定会执行。所以啊。在这4中模式中LAZY和UNDISPATCHED是一定执行协程的两种总启动模式。
最后
协程是个挺有意思的东西。笔者这里可能理解不全面。有错误。请指正。错误的文章会误导更多的人。
笔者之前说调度器是一个队列。就不准确。不过是为了让人更清楚理解每种启动模式。我想到的一个替代品.切不可直接就任务调度器CoroutineContext就是队列。
好了以上就是本期带来的 协程的启动模式。