目次
サスペンディングファンクションの生成についてドキュメントを読みました。
検証環境
- Kotlin 1.3 M2
- OpenJDK 10.0.2
サスペンディングファンクションにはいくつかの作り方があります。
デフォルトでシーケンシャル
他の場所で定義された2つのサスペンド関数があるとします。 具体的には、1秒間だけ遅延する2つの関数を定義します。
1 2 3 4 5 6 7 8 9 |
suspend fun doSomethingUsefulOne(): Int { delay(1000L) return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) return 29 } |
これらを順次実行する場合、どのように行うでしょうか? 最初に doSomethingUsefulOne
を実行し、 その次に doSomethingUsefulTwo
を実行し、 最後にそれらの返り値を合計するとしたらどうでしょうか。 実際には、 最初の関数を実行し、その結果によって次の関数を実行するか決めたり、 実行方法そのものを決めることもあります。
ここでは順次実行してみます。 コルーチン内のコードはそうでない標準的なコードと同じように上から順に実行されます。
1 2 3 4 5 6 7 8 |
fun main(args: Array<String>) = runBlocking<Unit> { val time = measureTimeMillis { val one = doSomethingUsefulOne() val two = doSomethingUsefulTwo() println("The answer is ${one + two}") } println("Completed in $time ms") } |
次のように出力されます。
1 2 |
The answer is 42 Completed in 2017 ms |
async を使った同時実行
もし、 2つの関数 doSomethingUsefulOne
と doSomethingUsefulTwo
の間に関係がないとしたらどうでしょうか。 できるだけ速く結果を出したい、 そのような場合には async
が使えます。
概念的には async
は launch
と同じです。 async
は分離されたコルーチン、 他のコルーチンと同時に実行可能な計量スレッドを開始します。 異なるのは launch
が 結果の値を保持しない Job
を返すのに対し、 async
が Deferred
を返すことです。 Deferred
は計量なブロックしないオブジェクトで、 後に必ず結果を返します。 Deferred
のメソッド .await()
を使って最終的な結果を得ることができます。 Deferred
もまた Job
であるため、 必要があればキャンセルできます。
1 2 3 4 5 6 7 8 |
fun main(args: Array<String>) = 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") } |
このコードの出力は次のようになります。
1 2 |
The answer is 42 Completed in 1073 ms |
2つの関数を同時に実行しているため、上のサンプルのおよそ半分の時間で結果が得られました。
レイジースタートの async
async
には オプショナルパラメータ start
に CoroutineStart.LAZY
を渡すと使える 怠惰オプション (laziness option) というのがあります。 それを使うと コルーチン は、 await
によって結果が求められた場合 または start
関数 が呼び出された場合にのみ実行されます。
1 2 3 4 5 6 7 8 9 10 11 |
fun main(args: Array<String>) = 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") } |
2つのコルーチンを定義し、それぞれの start
メソッド を呼び出して実行し、 最後に await
でそれぞれの結果を取得しています。
出力結果は次のようになります。
1 2 |
The answer is 42 Completed in 1049 ms |
もし、 println
の中で await
を呼び出し、 それぞれのコルーチンの start
関数呼び出し を省略した場合、 await
がそれぞれのコルーチンを実行しますが、結果が出力されるまで待つことになるので、 関数を逐次実行した場合と同じ挙動になります。 もちろん、 怠惰(laziness)の意図とは異なります。
start
関数 の呼び出しを省略すると次のようなコードになります。
1 2 3 4 5 6 7 8 |
fun main(args: Array<String>) = runBlocking<Unit> { val time = measureTimeMillis { val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms") } |
そして結果は次のようになります。 実行時間は逐次実行の時と変わりません。
1 2 |
The answer is 42 Completed in 2044 ms |
async(start = CoroutineStart.LAZY)
のユースケースは 値の計算にサスペンディングファンクションが含まれている場合の標準的な lazy
関数 を置き換えるものです。
Async-style 関数
ここで紹介する方法は、他のプログラミング言語で一般的になっているから紹介しているもので、後述の理由により Kotlin においては推奨されません。
明示的な GlobalScope
への参照と async コルーチンビルダ を使ってdoSomethingUserfulOne
と doSomethingUsefulTwo
を非同期で実行する 非同期スタイルの関数を定義することができます。 関数に “Async” という接尾辞をつけて、 非同期計算のみを開始し、その結果として得られる値を使用するということを明確にしています。
1 2 3 4 5 6 7 8 9 |
// The result type of somethingUsefulOneAsync is Deferred<Int> fun somethingUsefulOneAsync() = GlobalScope.async { doSomethingUsefulOne() } // The result type of somethingUsefulTwoAsync is Deferred<Int> fun somethingUsefulTwoAsync() = GlobalScope.async { doSomethingUsefulTwo() } |
これらの xxxxAsync
という関数はサスペンディングファンクションではありません。 どこからでも利用可能です。 ただし、それらを起動するコードとそれら関数による動作が非同期で進行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fun main(args: Array<String>) { 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") } |
出力結果は次のようになります。
1 2 |
The answer is 42 Completed in 1374 ms |
Consider what happens if between val one = somethingUsefulOneAsync()
の行と one.await()
式 の間に何らかの論理エラーがあり、プログラムが例外をスローし、実行されていた操作が異常終了した場合、どうなるか考えてみましょう。 通常、グローバルエラーハンドラが例外を捕捉しますが、このプログラム自体は終了することなく他の処理の実行を続けることができます。 Async-style 関数を使った上のプログラムの場合には、それを開始した操作が中止されるという事実にもかかわらず、バックグラウンドで somethingUsefulOneAsync
が実行中の状態で残り続けます。 この問題は、次のセクションで示すように、構造化された並行処理では発生しません。
async を使った構造化された並行処理
async を使った同時実行の例を用いて、 doSomethingUsefulOne
と doSomethingUsefulTwo
を同時に実行し、 その結果の合計を返す関数を外部化しましょう。 async コルーチンビルダ は、CoroutineScope
の拡張として定義されているため、 スコープ内にそれを配置する必要があります。 これが coroutineScope
の機能です。
1 2 3 4 5 |
suspend fun concurrentSum(): Int = coroutineScope { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } one.await() + two.await() } |
concurrentSum
の中で何かよくないことが起きて例外がスローされた場合には、 そのスコープで起動された全てのコルーチンがキャンセルされます。
実行結果は次の通りです。 2つの関数が同時実行されていることがわかります。
1 2 |
The answer is 42 Completed in 1035 ms |
コルーチンのキャンセルは階層的に伝播されます。 次のサンプルコードを実行すると、 下位のコルーチンがスローした例外によって、関連するコルーチンおよび上位のコルーチンもキャンセルされることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
fun main(args: Array<String>) = 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 2 3 |
Second child throws an exception First child was cancelled Computation failed with ArithmeticException |