Coroutineのキャンセルとタイムアウト

kotlinlang.org

コルーチンの実行をキャンセル

Cancelling coroutine execution

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i -> 
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }

    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel() // コルーチンのキャンセル
    job.join()   // コルーチンの完了を待つ
    println("main: Now I can quit.")
}

/*
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
*/

コルーチンのキャンセルについては、cancelAndJoin関数も用意されている。

コルーチンキャンセルは協同する

Cancellataion is cooperative

kotlinx.coroutinesに登録されているサスペンド関数は全てキャンセルすることができる。 コルーチンがキャンセルされると、CancellationExceptionがthrowされる。 しかしながら、実行中のコルーチンはキャンセルすることはできない。

そこでキャンセルのあとでjoin()することによって完了を待機する。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 計算ループ
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // キャンセルと完了待ち
    println("main: Now I can quit.")
}
/*
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
*/

最後のjoinがなければ、待機しない

val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // 計算ループ
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // AndJoin() キャンセルのみにすると
println("main: Now I can quit.")

/* 完了を待機しなければ
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
*/

計算コードをキャンセルできるようにする

  1. yield関数を使って、キャンセルされたかどうかを確認するサスペンド関数を用意する
  2. ループする際に、キャンセルされていないかどうか確認するようにする
// while (i < 5) {
while (isActive) { // cancellable computation loop
    // print a message twice a second
    if (System.currentTimeMillis() >= nextPrintTime) {
        println("job: I'm sleeping ${i++} ...")
        nextPrintTime += 500L
    }
}

finnalyを使ってリソースを閉じる

キャンセルができるサスペンド関数は、キャンセル時に、CancellationExceptionをthrowするので、ハンドルすることができる。

例として、try {...} finally {...}式とKotlinのuse関数を使う。

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        //  キャンセルもしくは、try内部完了後に呼び出される
        println("job: I'm runnning finally")
    }
}
delay(1300L) // yeildされる
println("main: I'm tired of waiting!")
job.cancelAndJoint() // キャンセルと完了を待機する
println("main: Now I can quit.")

/*
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
*/

キャンセルできないブロックを実行する

コルーチンが一度キャンセルされると、内部でサスペンド関数は全てキャンセルされ、実行することができないが、

サスペンド関数 withContext(NonCancellable) {...} を使うことで、finallyの後でもサスペンド関数を呼び出すことができる。

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        // キャンセル後に
        withContext(NonCancellable) {
            println("job: I'm running finally")
            delay(1000L)
            println("job: And I've just delayed for 1 sec because I'm non-cancellable")
        }
    }
}

Timeout

withTimeout サスペンド関数は、タイムアウトを設定することができる。

fun main() = runBlocking {
    launch {
        try {
            // 5Sでタイムアウトし、キャンセルされ、例外がスローされる
            withTimeout(5000L) {
                repeat(1000) { i ->
                    println("I'm sleeping $i ...")
                    delay(500L)
                }
            }
        } catch(e: TimeoutCancellationException) { // CancellationExceptionのサブクラス
            // タイムアウト時の処理
        } 
    }
    println("main end")
}

キャンセルはただの例外であるため、CancellationExceptionをハンドリングしなくても全てのリソースは通常通り閉じられる。

fun main() = runBlocking {
    launch {
        // 5Sでタイムアウトし、キャンセルされ、例外がスローされる
        withTimeout(5000L) {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
    }
    println("main end")
}