Kotlin 1.3: コルーチンコンテキストとディスパッチャ (1/3)


Kotlin コルーチン についてのドキュメント、 コンテキストとディスパッチャのセクションを読みました。 分量が多いため、3つの記事に分けて記述しました。

コルーチンは常に、 Kotlin 標準ライブラリ で定義されている CoroutineContext 型 の値によって表される、あるコンテキストで実行されます。

コルーチンのコンテキストは、様々な要素の集合です。 主な要素はこれまでに見たコルーチンの Job と そのディスパッチャです。 ディスパッチャをこのセクションで説明します。

検証環境

  • Kotlin 1.3.0-rc-146
  • kotlinx-coroutines-core 0.30.2

ディスパッチャとスレッド

コルーチンのコンテキストは コルーチンディスパッチャ を含んでいます。 コルーチンディスパッチャは対応するコルーチンが使用するスレッドを決定します。 コルーチンディスパッチャはコルーチンの実行を特定のスレッドに限定したり、スレッドプールにディスパッチしたり、スレッドプールに非同期で実行させることができます。

launchasync をはじめとする 全てのコルーチンビルダでは オプショナルの CoroutineContext パラメータ が使用できます。 それによって新しいコルーチンとその他のコンテキスト要素のディスパッチャを明示的に指定することができます。

例として次のコードを実行してみます。

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

launch { ... } を引数なしで実行した場合、 launch が実行された CoroutineScope を引き継ぎます。 サンプルコードでは、 メインスレッドで動くメイン関数内の runBlockng コルーチン のコンテキストを継承します。

Dispatchers.Unconfined は特別なディスパッチャで、 引数に渡した場合は メインスレッド で実行されます。 しかしそれは後述するように異なるメカニズムです。

デフォルトディスパッチャ (Dispatchers.Default) は コルーチンが GlobalScope で起動された時に使われ、 共通のバックグラウンドスレッドプールを利用します。 すなわち launch(Dispatchers.Default) { ... }GlobalScope.launch { ... } を実行した時と同じディスパッチャを使います。

newSingleThreadContext 関数 はコルーチンのために新しいスレッドを作成します。 専用のスレッドは非常に高価なリソースです。 実際のアプリケーションでは、不要になったときに解放するか、close 関数 を使用するか、 トップレベルの変数に格納してアプリケーション全体で再利用する必要があります。

実験として次のコードを実行してみます。

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

Dispatchers.Default を利用した時はプログラムが自動的にスレッドを選択しているのがわかります。

Unconfined デイスパッチャ と Confined ディスパッチャ

コルーチンディスパッチャ Dispatchers.Unconfined は コルーチンを呼び出し元のスレッドで実行します。 しかしそれは最初のサスペンドポイント(一時停止地点)までです。 サスペンド後は 呼び出されたサスペンディングファンクションによって決定されたスレッドで処理を再開します。 Unconfined ディスパッチャ は コルーチン が CPU時間 を消費せず、 特定のスレッドに固定化されている UI のような共通のデータを更新しない場合に適しています。

一方で、 デフォルトでは、 外部の CoroutineScope の ディスパッチャ は継承されます。 特に、 runBlocking コルーチン の デフォルト の ディスパッチャ は 呼び出し元のスレッドに固定化されていますので、 継承することで 予測可能な先入先出(FIFO)スケジューリングで当該スレッドに実行権限を限定することができます。

次のコードを実行して、挙動を確認します。

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

Unconfined ディスパッチャ を使った処理は、 サスペンド関数 (delay) が実行されるまでは呼び出し元と同じスレッドで、 その後は異なるスレッド(DefaultExecutorスレッド)で実行されています。

Unconfined ディスパッチャ は 高度なメカニズムです。 コルーチンの操作はすぐに実行する必要があるため、 後で実行するためのコルーチンのディスパッチが不要であるまたは望まない副作用を生じるという特定のコーナーケースで役立ちます。 一般的なコードでは、 Unconfined ディスパッチャ を使用しない方がいいです。

コルーチンとスレッドのデバッグ

コルーチンは、ひとつのスレッドの上で停止し他のスレッドで処理を再開することができます。 シングルスレッドのディスパッチャでも、 コルーチンがそのとき「どこ」で「なに」をしているのか判別するのは難しいでしょう。 一般的な、 スレッドを使ったアプリケーションのデバッグ手法は、 スレッド名をログファイルに書き出すことです。 この昨日は広くログ書き込みのフレームワークでサポートされています。 コルーチンを使う場合は、スレッド名だけは状況を十分に表示できませんから、 kotlinx.coroutines が簡単にデバッグするための機構を含んでいます。

次のコードを、 JVM オプション -Dkotlinx.coroutines.debug をつけて実行してみます。

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

3つのコルーチンが出力されています。 ひとつめ “#1” は メインのコルーチンで、 runBlocking で生成されたものになります。 他のコルーチンは 値 a を計算するもの (“#2”) と 値 b を計算するもの (“#3”) で、 runBlocking のコンテキストで実行されており、 メインスレッドに固定されています。

もし オプション -Dkotlinx.coroutines.debug をつけないと、 出力は次のようになります。 コルーチンの番号が出力されません。

Kotlin 1.3: コルーチンコンテキストとディスパッチャ (2/3)に続きます。