Kotlin 1.3: サスペンディングファンクションの生成


サスペンディングファンクションの生成についてドキュメントを読みました。

検証環境

  • Kotlin 1.3 M2
  • OpenJDK 10.0.2

サスペンディングファンクションにはいくつかの作り方があります。

デフォルトでシーケンシャル

他の場所で定義された2つのサスペンド関数があるとします。 具体的には、1秒間だけ遅延する2つの関数を定義します。

これらを順次実行する場合、どのように行うでしょうか? 最初に doSomethingUsefulOne を実行し、 その次に doSomethingUsefulTwo を実行し、 最後にそれらの返り値を合計するとしたらどうでしょうか。 実際には、 最初の関数を実行し、その結果によって次の関数を実行するか決めたり、 実行方法そのものを決めることもあります。

ここでは順次実行してみます。 コルーチン内のコードはそうでない標準的なコードと同じように上から順に実行されます。

次のように出力されます。

async を使った同時実行

もし、 2つの関数 doSomethingUsefulOnedoSomethingUsefulTwo の間に関係がないとしたらどうでしょうか。 できるだけ速く結果を出したい、 そのような場合には async が使えます。

概念的には asynclaunch と同じです。 async は分離されたコルーチン、 他のコルーチンと同時に実行可能な計量スレッドを開始します。 異なるのは launch が 結果の値を保持しない Job を返すのに対し、 asyncDeferred を返すことです。 Deferred は計量なブロックしないオブジェクトで、 後に必ず結果を返します。 Deferred のメソッド .await() を使って最終的な結果を得ることができます。 Deferred もまた Job であるため、 必要があればキャンセルできます。

このコードの出力は次のようになります。

2つの関数を同時に実行しているため、上のサンプルのおよそ半分の時間で結果が得られました。

レイジースタートの async

async には オプショナルパラメータ startCoroutineStart.LAZY を渡すと使える 怠惰オプション (laziness option) というのがあります。 それを使うと コルーチン は、 await によって結果が求められた場合 または start 関数 が呼び出された場合にのみ実行されます。

2つのコルーチンを定義し、それぞれの start メソッド を呼び出して実行し、 最後に await でそれぞれの結果を取得しています。

出力結果は次のようになります。

もし、 println の中で await を呼び出し、 それぞれのコルーチンの start 関数呼び出し を省略した場合、 await がそれぞれのコルーチンを実行しますが、結果が出力されるまで待つことになるので、 関数を逐次実行した場合と同じ挙動になります。 もちろん、 怠惰(laziness)の意図とは異なります。

start 関数 の呼び出しを省略すると次のようなコードになります。

そして結果は次のようになります。 実行時間は逐次実行の時と変わりません。

async(start = CoroutineStart.LAZY) のユースケースは 値の計算にサスペンディングファンクションが含まれている場合の標準的な lazy 関数 を置き換えるものです。

Async-style 関数

ここで紹介する方法は、他のプログラミング言語で一般的になっているから紹介しているもので、後述の理由により Kotlin においては推奨されません。

明示的な GlobalScope への参照と async コルーチンビルダ を使ってdoSomethingUserfulOnedoSomethingUsefulTwo を非同期で実行する 非同期スタイルの関数を定義することができます。 関数に “Async” という接尾辞をつけて、 非同期計算のみを開始し、その結果として得られる値を使用するということを明確にしています。

これらの xxxxAsync という関数はサスペンディングファンクションではありません。 どこからでも利用可能です。 ただし、それらを起動するコードとそれら関数による動作が非同期で進行します。

出力結果は次のようになります。

Consider what happens if between val one = somethingUsefulOneAsync() の行と one.await() 式 の間に何らかの論理エラーがあり、プログラムが例外をスローし、実行されていた操作が異常終了した場合、どうなるか考えてみましょう。 通常、グローバルエラーハンドラが例外を捕捉しますが、このプログラム自体は終了することなく他の処理の実行を続けることができます。 Async-style 関数を使った上のプログラムの場合には、それを開始した操作が中止されるという事実にもかかわらず、バックグラウンドで somethingUsefulOneAsync が実行中の状態で残り続けます。 この問題は、次のセクションで示すように、構造化された並行処理では発生しません。

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

async を使った同時実行の例を用いて、 doSomethingUsefulOnedoSomethingUsefulTwo を同時に実行し、 その結果の合計を返す関数を外部化しましょう。 async コルーチンビルダ は、CoroutineScope の拡張として定義されているため、 スコープ内にそれを配置する必要があります。 これが coroutineScope の機能です。

concurrentSum の中で何かよくないことが起きて例外がスローされた場合には、 そのスコープで起動された全てのコルーチンがキャンセルされます。

実行結果は次の通りです。 2つの関数が同時実行されていることがわかります。

コルーチンのキャンセルは階層的に伝播されます。 次のサンプルコードを実行すると、 下位のコルーチンがスローした例外によって、関連するコルーチンおよび上位のコルーチンもキャンセルされることがわかります。

実行結果は次のようになります。