Skip to content

サスペンド関数の構成

このセクションでは、サスペンド関数のさまざまな構成方法について説明します。

デフォルトで順次実行

リモートサービス呼び出しや計算のような有用な処理を行う2つのサスペンド関数が、別の場所で定義されていると仮定しましょう。ここではそれらが有用であると仮定しますが、実際にはこの例のためにそれぞれが1秒間遅延するだけです。

kotlin
suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

もしこれらを_順次_呼び出す必要がある場合 — つまり、まずdoSomethingUsefulOneを呼び出し、次にdoSomethingUsefulTwoを呼び出し、その結果の合計を計算する場合、どうすればよいでしょうか?実際には、最初の関数の結果を使用して、2番目の関数を呼び出す必要があるか、あるいはどのように呼び出すかを決定する場合に、この方法を使用します。

コルーチン内のコードは、通常のコードと同様に、デフォルトで_順次_実行されるため、通常の順次呼び出しを使用します。次の例では、両方のサスペンド関数の実行にかかる合計時間を測定することで、これを実演します。

kotlin
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

完全なコードはこちらで入手できます。

これは次のような出力を生成します:

text
The answer is 42
Completed in 2017 ms

asyncを使った並行処理

もしdoSomethingUsefulOnedoSomethingUsefulTwoの呼び出し間に依存関係がなく、両方を_並行して_実行することで、より早く答えを得たい場合はどうでしょうか?ここでasyncが役に立ちます。

概念的には、asynclaunchと同じです。これは、他のすべてのコルーチンと並行して動作する軽量スレッドである、個別のコルーチンを開始します。違いは、launchJobを返し、結果値を持ちませんが、asyncDeferred — 後で結果を提供することを約束する軽量な非ブロッキングのFuture(フューチャー) — を返すことです。deferred値に対して.await()を使用すると、最終的な結果を取得できますが、DeferredJobなので、必要に応じてキャンセルできます。

kotlin
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

完全なコードはこちらで入手できます。

これは次のような出力を生成します:

text
The answer is 42
Completed in 1017 ms

2つのコルーチンが並行して実行されるため、これは2倍高速です。コルーチンにおける並行処理は常に明示的であることに注意してください。

遅延開始のasync

オプションとして、asyncstartパラメータをCoroutineStart.LAZYに設定することで、遅延実行にできます。このモードでは、awaitによって結果が必要とされたとき、またはそのJobstart関数が呼び出されたときにのみコルーチンが開始されます。次の例を実行してください。

kotlin
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // some computation
        one.start() // start the first one
        two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

完全なコードはこちらで入手できます。

これは次のような出力を生成します:

text
The answer is 42
Completed in 1017 ms

このように、ここでは2つのコルーチンが定義されていますが、前の例のようにすぐに実行されるわけではなく、startを呼び出すことで、いつ正確に実行を開始するかをプログラマが制御できます。まずoneを開始し、次にtwoを開始し、その後個々のコルーチンが終了するのを待機します。

個々のコルーチンで最初にstartを呼び出さずにprintlnawaitを呼び出すだけの場合、awaitはコルーチンの実行を開始し、その完了を待機するため、これは順次的な振る舞いにつながることに注意してください。これは、遅延実行の意図されたユースケースではありません。async(start = CoroutineStart.LAZY)のユースケースは、値の計算にサスペンド関数が関与する場合において、標準のlazy関数の代替となります。

asyncスタイルの関数

このasync関数を使ったプログラミングスタイルは、他のプログラミング言語で一般的なスタイルであるため、ここでは例示のためだけに提供されています。Kotlinコルーチンでこのスタイルを使用することは、以下の理由により強く推奨されません

asyncコルーチンビルダーを使用し、構造化された並行処理をオプトアウトするためにGlobalScope参照を使用することで、doSomethingUsefulOnedoSomethingUsefulTwoを_非同期に_呼び出すasyncスタイルの関数を定義できます。そのような関数には「...Async」というサフィックス(接尾辞)を付け、それらが非同期計算を開始するだけで、結果を得るには生成されたdeferred値を使用する必要があるという事実を強調します。

GlobalScopeは、些細ではない方法で裏目に出る可能性があるデリケートなAPIであり、そのうちの1つは以下で説明されます。そのため、GlobalScopeを使用するには@OptIn(DelicateCoroutinesApi::class)で明示的にオプトインする必要があります。

kotlin
// The result type of somethingUsefulOneAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

これらのxxxAsync関数は、_サスペンド_関数ではないことに注意してください。これらはどこからでも使用できます。ただし、これらを使用すると、常に呼び出し元のコードとの非同期(ここでは_並行_を意味します)でのアクションの実行が伴います。

次の例は、コルーチンの外部でのそれらの使用方法を示しています:

kotlin
import kotlinx.coroutines.*
import kotlin.system.*

// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

完全なコードはこちらで入手できます。

もしval one = somethingUsefulOneAsync()の行とone.await()の式の間にコードに何らかの論理エラーがあり、プログラムが例外をスローし、プログラムによって実行されていた操作が中断された場合に何が起こるかを考えてみましょう。通常、グローバルなエラーハンドラーがこの例外をキャッチし、エラーを開発者向けにログに記録して報告できますが、プログラムは他の操作を続行できます。しかし、ここでは、somethingUsefulOneAsyncを開始した操作が中断されたにもかかわらず、それがバックグラウンドでまだ実行されています。この問題は、以下のセクションで示すように、構造化された並行処理では発生しません。

asyncを使った構造化された並行処理

asyncを使った並行処理の例を、doSomethingUsefulOnedoSomethingUsefulTwoを並行して実行し、それらの結合された結果を返す関数にリファクタリングしてみましょう。asyncCoroutineScopeの拡張であるため、必要なスコープを提供するためにcoroutineScope関数を使用します:

kotlin
suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

このようにして、もしconcurrentSum関数のコード内で何かがうまくいかず、例外がスローされた場合、そのスコープ内で起動されたすべてのコルーチンがキャンセルされます。

kotlin
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")
}

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

完全なコードはこちらで入手できます。

上記のmain関数の出力から明らかなように、両方の操作は引き続き並行して実行されます:

text
The answer is 42
Completed in 1017 ms

キャンセルは常にコルーチン階層を通じて伝播されます:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // Emulates very long computation
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

完全なコードはこちらで入手できます。

子の1つ(つまりtwo)が失敗すると、最初のasyncと待機中の親の両方がキャンセルされることに注目してください:

text
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException