Coroutine 的三大要素不知道大家还记得吗?CoroutineScope、Suspend function、Dispatchers。
CoroutineScope 是定义 coroutine 执行的范围,我们可以使用 launch
、 async
来建立范围。
Suspend function 是用来处理非同步的执行,所以我们可以在这边放上耗时任务,Coroutine 执行到这边的时候,就会将此 coroutine 暂停(suspend/挂起),等到完成之後,才又会把此 coroutine 恢复运行。
而 Dispatchers 是用来指定该 coroutine 使用不同的调度器来执行,所谓调度器指的是 coroutine 根据不同的应用会在背景会建立不同的执行绪池/执行绪,使用者可以根据使用的情境来选择适当的 Dispatchers。如 Dispatchers.IO、Dispatchers.Default...
今天要来介绍的是那些内建的 suspend function,如我们之前经常使用的 delay()
就是其中一个成员喔。
第一个当然就是我们的 delay()
罗,以前我们使用执行绪的时候,如果我们需要在执行绪上暂停一下,我们会使用 Thread.sleep() 来把目前的执行绪停止,不过这样做的缺点就是整个执行绪就停起来了,这个执行绪就什麽事都不能做。
delay()
则是让这个 coroutine 进入等待的状态,coroutine 进入等待之後,就会去找寻在同一个 Dispatchers 的任务来执行。
fun main() = runBlocking<Unit> {
launch {
Thread.sleep(100L)
println("Thread 1 ${Thread.currentThread().name}")
}
launch {
println("Thread 2 ${Thread.currentThread().name}")
}
}
Coroutine 1 main
Coroutine 2 main
→ 在第一个 launch 中呼叫了 Thread.sleep(100L) 之後,执行绪就会暂停 100 毫秒,当暂停时间结束过後,才会开始执行後面的内容。所以输出会是 Coroutine 1 main → Coroutine 2 main
如果在 coroutine 里面使用 Thread.sleep()
,会显示一个警告。
就是在告诉你 coroutine 不要使用会阻塞的方法啦。
delay()
fun main() = runBlocking<Unit> {
launch {
delay(100L)
println("Coroutine 1 ${Thread.currentThread().name}")
}
launch {
println("Coroutine 2 ${Thread.currentThread().name}")
}
}
Coroutine 2 main
Coroutine 1 main
→ 第一个 launch 呼叫 delay()
之後,这一个 launch
的 coroutine 的状态被切换成等待,然後就会执行下一段程序。当 delay()
结束之後,就会从暂停的地方恢复,所以就会继续往下执行。最後看到的输出结果就是 Coroutine 2 main -> Coroutine 1 main
如果直接查字典,可能会得到一个不太贴切的翻译:屈服。
根据韦氏辞典的解释,我认为比较贴切的是这个解释
to give up possession of on claim or demand - 根据主张或需求放弃权利。
那麽,到底 yield()
到底是什麽用途呢?
Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines on the same dispatcher to run if possible. - Ref
如果可能,放弃目前 coroutine 调度程序的执行绪/执行绪池到另一个在同一个调度器的 coroutine 。
还是很茫吗?看下面的范例:
launch
产生两个 coroutine - Ref
fun main() = runBlocking {
val job = launch {
val child = launch {
try {
println("run child")
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
println("run parent")
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()
}
run parent
run child
Cancelling child
Child is cancelled
Parent is not cancelled
→ 从 log 的输出我们可以发现,我们一开始会先从外侧的 coroutine 开始执行 ,所以印出了第一行 run parent
,当我们呼叫 yield()
时,此时执行绪的使用权就会切换至子 coroutine,所以列印出 run child
,接着在子 coroutine 中呼叫 delay()
,内部的 coroutine 切换至等待状态,并把执行绪使用权切回外层的 coroutine 并列印 Cancelling child
。接着,呼叫 child.cancel()
来把
子 coroutine 里面的任务给停掉,於是子 coroutine 的 delay()
被取消,列印出 Child is cancelled
。子 coroutine 被取消之後,调用 child.join()
就只会把子 coroutine 切回父 coroutine。下面的 yield()
则因为没有其他的任务等待,所以没有作用,最後列印 Parent is not cancelled
结束。
我们换另一个例子来看看:
fun main() = runBlocking{
val job1 = launch {
repeat(10) {
println("coroutine1: $it")
yield()
}
}
val job2 = launch {
repeat(10) {
println("coroutine2: $it")
yield()
}
}
job1.join()
job2.join()
}
猜猜看,这段程序码会怎麽输出呢?
如果按照我们前面所说的, yield()
会把放弃目前 coroutine 的执行绪到另一个 coroutine,所以当执行到 yield()
时,就会把执行的权利交给下一个 coroutine 来执行。所以答案会是
coroutine1: 0
coroutine2: 0
coroutine1: 1
coroutine2: 1
coroutine1: 2
coroutine2: 2
coroutine1: 3
coroutine2: 3
coroutine1: 4
coroutine2: 4
coroutine1: 5
coroutine2: 5
coroutine1: 6
coroutine2: 6
coroutine1: 7
coroutine2: 7
coroutine1: 8
coroutine2: 8
coroutine1: 9
coroutine2: 9
另外,假如 coroutine 在暂停的时候被取消,那麽纵使呼叫了 yield()
也没有办法回去,毕竟都被取消了。
fun main() = runBlocking {
val job1 = launch {
repeat(10) {
println("coroutine1: $it")
yield()
}
}
val job2 = launch {
repeat(10) {
println("coroutine2: $it")
job1.cancel() //<- Add this line
yield()
}
}
job1.join()
job2.join()
}
coroutine1: 0
coroutine2: 0
coroutine2: 1
coroutine2: 2
coroutine2: 3
coroutine2: 4
coroutine2: 5
coroutine2: 6
coroutine2: 7
coroutine2: 8
coroutine2: 9
在 job2
里面调用 job1.cancel()
取消 Job1,当在 job2
呼叫 yield()
也没有办法切回 Job1。
delay() 与 yield() 使用上的结果看起来有点相像,不过实际的内容是不太一样的, delay()
是会让目前的 coroutine 切换成等待状态,接着就会去寻找下一个等待执行的 coroutine ,因为它只是让 coroutine 等待,所以执行绪并没有被停止下来,跟 Thread.sleep()
是不一样的, Thread.sleep()
会阻塞执行绪,所以後面就算有任务需要执行,也会因为执行绪被卡住的关系而无法执行。
yield()
则是放弃目前执行的权利,让下一个 coroutine 可以执行(需要同一个调度器),所以就实现来说, yield()
与 delay()
都可以做到暂停目前 coroutine 的任务,不过实际运用上还是有些不同。
内建的 suspend 函式就先介绍这两个,其他的往後几篇再继续介绍。
<<: Golang 转生到web世界 - template
今天就来个说个在新手时期很常遇到,但却不知为什麽会发生的问题 来看一下我们前几天的表单范例,与图上 ...
两个人都准备好的时候,要转到游戏画面 我来把准备画面跟游戏分开好了 这样比较不会什麽都塞在同一个 l...
这个插件就如同名称一样,是专门寄信使用的(恩对,介绍就这样而已)。 准备 在开始使用之前要先做好前置...
10.5 Seidel’s APSP 演算法 如果一个无向图的所有边都没有权重,那麽就能用奥地利出生...
本文将於赛後同步刊登於笔者部落格 有兴趣学习更多 Kubernetes/DevOps/Linux 相...