yield() 字面上的意思是「讓出」。在 Coroutine 中,它的作用是:如果可能,把目前 Coroutine 占用的執行緒(或執行緒池)讓給同一個 Dispatcher 上的其他 Coroutine 去執行。

在巢狀 Coroutine 中觀察 yield#

下面的範例由外層 launch 包住一個內層 launch,藉此看清楚 yield() 何時把執行權交出去。

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

順序解讀:

  • 從外層 Coroutine 開始跑,先印出 run parent
  • 呼叫 yield() 把執行權交給子 Coroutine,於是印出 run child,子 Coroutine 接著進入 delay(Long.MAX_VALUE) 等待,執行權回到外層。
  • 外層印出 Cancelling child,呼叫 child.cancel() 取消子 Coroutine,子 Coroutine 的 finally 印出 Child is cancelled
  • 第二個 yield() 因為已經沒有其他可執行的同層 Coroutine,沒有實際讓出的對象,外層直接印出 Parent is not cancelled

兩個 launch 交錯輸出#

把兩個 launch 都加入 yield(),可以觀察到兩個 Coroutine 交錯執行:

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() 都把執行權交給對方,所以輸出會是 coroutine1: 0coroutine2: 0coroutine1: 1coroutine2: 1 ⋯⋯ 一路交錯到 9。

被取消的 Coroutine 不會回來#

如果一個 Coroutine 已經被取消,即使它原本應該因為 yield() 被叫回來執行,也不會再恢復。

fun main() = runBlocking {
    val job1 = launch {
        repeat(10) {
            println("coroutine1: $it")
            yield()
        }
    }
    val job2 = launch {
        repeat(10) {
            println("coroutine2: $it")
            job1.cancel()
            yield()
        }
    }
    job1.join()
    job2.join()
}

job2 內呼叫了 job1.cancel(),之後即使呼叫 yield(),也沒有 job1 可以切換回去,因此最終只會看到 coroutine1: 0 一次,後面全部都是 coroutine2 的輸出。

yield 與 delay 的差異#

兩者表面上看起來都讓 Coroutine 暫停,實際上意義不同:

  • delay():把目前 Coroutine 設為等待,等指定的時間到才恢復。
  • yield():放棄當下的執行權,讓同一個 Dispatcher 上其他可執行的 Coroutine 接手;如果沒有其他人可接手,就直接繼續往下執行。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10266141