Day12:内建的 suspend 函式,好函式不用吗?(1)

Coroutine 的三大要素不知道大家还记得吗?CoroutineScope、Suspend function、Dispatchers。

CoroutineScope 是定义 coroutine 执行的范围,我们可以使用 launchasync 来建立范围。

Suspend function 是用来处理非同步的执行,所以我们可以在这边放上耗时任务,Coroutine 执行到这边的时候,就会将此 coroutine 暂停(suspend/挂起),等到完成之後,才又会把此 coroutine 恢复运行。

而 Dispatchers 是用来指定该 coroutine 使用不同的调度器来执行,所谓调度器指的是 coroutine 根据不同的应用会在背景会建立不同的执行绪池/执行绪,使用者可以根据使用的情境来选择适当的 Dispatchers。如 Dispatchers.IO、Dispatchers.Default...

今天要来介绍的是那些内建的 suspend function,如我们之前经常使用的 delay() 就是其中一个成员喔。


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

Inappropriate blocking method call

如果在 coroutine 里面使用 Thread.sleep() ,会显示一个警告。

就是在告诉你 coroutine 不要使用会阻塞的方法啦。
Inappropriate blocking method call

同样的程序码,我们改用 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


yield()

如果直接查字典,可能会得到一个不太贴切的翻译:屈服。
屈服?

根据韦氏辞典的解释,我认为比较贴切的是这个解释

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 。

还是很茫吗?看下面的范例:

  • 一个巢状的 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

>>:  [想试试看JavaScript ] 事件处理

Angular 冒泡事件

今天就来个说个在新手时期很常遇到,但却不知为什麽会发生的问题 来看一下我们前几天的表单范例,与图上 ...

22 准备完成後跳转到游戏页面

两个人都准备好的时候,要转到游戏画面 我来把准备画面跟游戏分开好了 这样比较不会什麽都塞在同一个 l...

Day 24 Flask-Mail

这个插件就如同名称一样,是专门寄信使用的(恩对,介绍就这样而已)。 准备 在开始使用之前要先做好前置...

最短路径问题 (4)

10.5 Seidel’s APSP 演算法 如果一个无向图的所有边都没有权重,那麽就能用奥地利出生...

Day 19 - Rancher App(v2.5) 介绍

本文将於赛後同步刊登於笔者部落格 有兴趣学习更多 Kubernetes/DevOps/Linux 相...