ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Java (JVM 言語) におけるコードカバレッジの計測方法

Java と Kotlin で書かれたアプリケーションのコードテストのカバレッジを取りたいなーと思って、コードカバレッジ計測ツールについて調べてみてる。

JVM 言語におけるコードカバレッジ計測の方法

JaCoCo のドキュメントにわかりやすくまとまっている。

Coverage information has to be collected at runtime. For this purpose JaCoCo creates instrumented versions of the original class definitions. The instrumentation process happens on-the-fly during class loading using so called Java agents.

There are several different approaches to collect coverage information. For each approach different implementation techniques are known. The following diagram gives an overview with the techniques used by JaCoCo highlighted:

f:id:nobuoka:20180228230512p:plain

JaCoCo - Implementation Design

カバレッジ情報は実行時に収集されるものなので、情報収集のための仕組みが必要。

大きく分けると Runtime Profiling と Instrumentation の 2 つの仕組みがある。 前者は JVM の仕組み (JVMTI や JVMPI) を使うもの。 後者は実行対象のプログラムの方に収集用の仕組みを搭載する (これを instrumentation というらしい?) もの。

Runtime Profiling

JVM TI は Java Virtual Machine Tool Interface の略。 Java SE 5 で導入された。

JVM TI は JVM 上で動くアプリケーションの状態を検査したり、実行を制御したりするためのプログラミングインターフェイスらしい。

JVMPI は JVM TI よりも古くからある同じようなインターフェイスらしい。 Java SE 5 で (JVM TI ができたことで) 非推奨になり、Java SE 6 で廃止された模様。

これらを使ったコードカバレッジは、アプリケーション側に何も手を入れなくて良いことが利点だと思われる。 が、詳細はわからない。

Instrumentation

上でも説明したように、計測対象のアプリケーションコードに情報収集のための仕組みを搭載する方法が、この Instrumentation である。 Runtime Profiling と異なり、Android アプリの実行環境のような非 JVM 環境でも使用できるという利点がありそう *1

上の図を見るとわかるように、様々な方法がある。

  • ソースコードを変更するもの。
  • バイトコードを変更するもの。
    • オフライン (offline) で変更するもの。 (= JVM に読み込まれる前の状態、例えばクラスファイルそのものを変更する。)
      • Replace と Inject って書かれてるけどそれぞれの意味はわからない。
    • オンザフライ (on-the-fly) で変更するもの。 (= JVM 読み込まれる際などにオンメモリで変更する。)
      • クラスローダで変更する方法と、Java Agent を用いる方法がある。

JaCoCo は、Java Agent を用いてオンザフライでバイトコードを変更する方式である。 *2

Java Agent について

Java Agent については java.lang.instrument パッケージの Javadoc に書かれている。

-javaagent:[=] というコマンドラインオプションで指定して使用できるものである。 次のページも参考になる。

エージェントの背景にある基本概念は、「JVM がクラスをロードする場合、エージェントはそのクラスのバイトコードを修正できる」 という考え方です。

それぞれの特徴

現時点で自分がわかっている範囲で特徴を書いておく。

  • ソースコードを変更する方式は、言語によって使用できるかどうかが変わる。 例えば Java 言語に対応しているツールでも Kotlin には対応していなかったりする。
    • 一方でバイトコードを変更する方式は、JVM 言語であればどれにでも対応できるはず。
  • オフラインで変更する方式は、ビルド時にクラスファイルが書き換えられてしまうので、それをそのまま本番アーティファクトのビルド時に使用できない。 (使用したらダメというわけではないが、パフォーマンスが落ちるなどの問題が起こる。)

様々なコードカバレッジツール

JVM 言語用の様々なコードカバレッジツールについて、Clover のブログで比較紹介してくれている。 (最終更新が 2017 年春なので情報はちょっと古いかもしれない。)

有名どころとしては JaCoCoOpenClover (Atlassian Clover がオープンソース化されたもの)、JCov といったところだと思う。

Instrumentation 方式に着目すると、JaCoCo と JCov はオフラインおよびオンザフライのバイトコード instrumentation に対応しており、Clover はソースファイル instrumentation に対応している。 なので JaCoCo や JCov は (JVM 言語なら何でも対応できるので) Kotlin にも対応するが、Clover は (対応言語に入っていない) Kotlin には対応しない。

Clover と JaCoCo を軽く使ってみたところ、設定の簡単さはどちらも同じ。 出力される HTML を見ると JaCoCo は単純な内容で、Clover の方はプロジェクトリスクの高いものを表示したり、視覚的だったりと、結果表示については高機能さを感じた。

今回は Kotlin でのカバレッジも取りたいので、(対応していない) Clover は選外で、JaCoCo か JCov のどっちかを使うことになりそう。

*1:憶測です

*2:オプションでオフラインでの instrumentation も可能。

Ktor の自動再読み込み (Automatic Reloading) 機能

JetBrains 製の Kotlin 用 web アプリケーションフレームワークKtor の自動再読み込み機能について。 開発時にサーバー全体を再起動しなくても、ソースコードを変更してビルドした後のクラスファイルの再読み込みをしてくれる機能である。

設定方法

上記ドキュメントにあるように、下記のように設定ファイルの ktor.deployment.watch に監視対象のモジュールを指定してやればよい。

ktor {
    deployment {
        port = 8080
        watch = [ module1, module2 ]
    }
    
    …
}

モジュールの値としてどんな文字列を指定してやればよいのか?

ドキュメントには、以下のように書かれている。

For now watch keys are just strings that are matched with contains against classpath entries of the loaded application, such as a jar name or a project directory name. These classes are then loaded with special ClassLoader that is recycled when change is detected.

Autoreload - Servers - Ktor

すなわち、JAR 名やプロジェクトのディレクトリ名などの、クラスパスのエントリに含まれるような文字列を書けばよい。

例えば Gradle を使っているプロジェクトを IntelliJ IDEA 上で実行する場合は、クラスパスに 「file:/C:/Users/nobuoka/Documents/projects/ktor-sample/app/out/production/classes/」 というようなファイル URL が含まれる。 この文字列の一部分にマッチするような文字列を指定してやる。 IntelliJ IDEA 用での実行だけを考えるなら、ぶっちゃけ 「/out/」 あたりを指定しておいてやればいい気がする。 それだけですべてのサブプロジェクトに自動再読み込みが適用される。

自動再読み込みの仕組みを追う

ドキュメントだけを読んでもあんまり理解できないので内部を追いかけたい人用。 (自分もドキュメントだけではわからなくて Ktor のソースコードを見ながらデバッグした。)

ソースコードを見ると、自動再読み込みのためのクラスローダの生成が ApplicationEngineEnvironmentReloading クラスで行われていた。 メソッドとしては createClassLoader メソッドである。 IntelliJ IDEA などの IDE を使っているならデバッグしやすいので、このクラスを IDE 上で開いてブレークポイントをはってデバッグ実行すると処理の流れを追える。

『No ktor.deployment.watch patterns specified, automatic reload is not active』 とか 『No ktor.deployment.watch patterns match classpath entries, automatic reload is not active』 といったメッセージもこのクラスで出力されている。

参考になれば。

Ktor で例外発生時に Sentry にクラッシュレポートを送る

最近、個人で web アプリケーションを書くのに Ktor (Kotlin の web アプリケーションフレームワーク) を使っています。

Ktor では、1 つ以上の 「インターセプタ (interceptor)」 から成る 「パイプライン (pipeline)」 にリクエストを通すことで、HTTP リクエストに対する処理が実行されます。 各インターセプタがリクエストに対して処理を行う、という形です。 パイプラインについての詳細なドキュメントはまだ未整備な状態 *1 ですが、Application の説明の方にパイプラインについても簡単に書かれています。

Sentry にクラッシュレポートを送る

今回は、後段のインターセプタで例外が発生した際に Sentry にクラッシュレポートを送るためのインターセプタを書いてみました。

Application クラスの intercept メソッド *2 を呼び、インターセプタを追加します。

fun Application.main() {
    // ... (略) ...
    Sentry.init(sentryDsn)
    // Application#intercept メソッドでインターセプタを追加する。
    intercept(ApplicationCallPipeline.Call) {
        try {
            proceed()
        } catch (e: Exception) {
            // 追加情報としてリクエストのエンドポイントの情報を与える。
            Sentry.getContext().addExtra(
                "Request endpoint",
                "${call.request.httpMethod.value} ${call.request.uri}")
            Sentry.capture(e)
            throw e
        } finally {
            Sentry.clearContext()
        }
    }
    // ... (略) ...

Sentry クラスなどは Sentry 公式の sentry ライブラリのものです。

悩み事

Ktor ではコルーチンが使われているため、各リクエストに対して 1 スレッドが割り当てられる、というわけではありません。 一方で、sentry ライブラリのコンテキストマネージャはデフォルトだとスレッドローカル変数を使用してコンテキストが分かれるような作りになっていて、Ktor の処理の中の様々な場所で Sentry のコンテキストを触ろうとするとスレッド違いなどを意識しないといけなくて難しそう、だと思ってます。

上の例では同じスレッド上でコンテキストを触っているので問題ないのですが、もうちょっと込み入ったことをしたくなると不便そうです。 コンテキスト管理をスレッドローカル変数でやるのは悪い文化。

Ktor でクラッシュレポートを Sentry に送るライブラリ

ライブラリもあります。 が、まだスナップショットでしかリリースしてなさそう? なので私は避けました。

本記事でのバージョン

  • Kotlin 1.2.10
  • Ktor 0.9.0
  • sentry 1.6.4

*1:ページとしては存在するが、まだ記載されていない

*2:Application クラスが継承している Pipeline クラスが実装している

Kotlin のコルーチン (coroutines) について学ぶ

KotlinConf 2017 の情報を追ったり、Ktor を見たりしているとコルーチンがよく出てくる。 コルーチンについては概要は知っているが詳細を追いかけていなかったので、コルーチンについて学んでメモ程度に記録しておく。

Kotlin 1.1 においてコルーチンは実験段階で、将来にはこのページの情報は古くなっているかもしれないので注意されたし。

本記事に書かれている内容

  • コルーチンとは何か
  • コルーチンの実装がどこにあるのか (言語サポートと標準ライブラリと外部ライブラリ)
  • コルーチンの基本的な使い方
  • コルーチンのキャンセル処理について
  • コルーチンコンテキストについて
  • チャンネル
  • 並行性の問題
  • Select 文

Coroutine (コルーチン) って何?

まずは公式リファレンスの情報を追う。

コルーチンは軽量なスレッドのようなもの。 スレッドの場合は、非同期処理を行う際に呼び出し側はスレッドをブロック (blocking) して待機するが、コルーチンの場合は非同期処理の呼び出しでコルーチンを中断 (suspension) することができる。 コルーチンの中断は、スレッドのブロックと比べて安く、より制御しやすい

Kotlin 言語やライブラリとコルーチンの関係

  • コルーチンは Kotlin 1.1 で実験的に言語機能として組み込まれた。
    • 言語サポート (suspending 関数)。
    • 低レベルなコア API : 標準ライブラリへの組み込み。 (kotlin.coroutines.experimental パッケージ; 正式リリース時には kotlin.coroutines に移動され、古いパッケージも互換性のために残される。)
  • コルーチンの高レベル制御 API は別のライブラリとして提供されている。 (kotlinx.coroutines)

言語機能 : suspending 関数 (suspending functions)

コルーチンに関する言語機能として、suspending 関数がある。 suspend 修飾子が付けられた関数である。

suspend fun foo(): Bar { ... }

このような関数の呼び出し時に、コルーチンの中断が発生する可能性がある。 (中断されない可能性もある。) Suspending 関数を呼ぶことができるのは、コルーチンの中や他の suspending 関数の中からだけである。

無名ラムダも suspending 関数になりえる。 kotlinx.coroutines に含まれる async 関数の宣言は以下のようになっており、引数のラムダは suspending 関数である。

fun <T> async(block: suspend () -> T)

上記関数に渡したラムダは suspending ラムダとなる。

コルーチンの低レベルなコア API について

コルーチンの低レベルなコア API は、主にコルーチンを扱うライブラリのためのもので、基本的にはアプリケーションコードでは使わない。 (buildSequencebuildIterator だけはアプリケーションコードからの使用が想定されているらしい。)

低レベルなコア API についての詳細は以下にある。

コルーチンの高レベル API について

コルーチンの高レベル APIGitHub リポジトリに置かれている。 実際にアプリケーション開発者が使うのはこちらのライブラリになる。

コルーチンの使い方 (高レベル API)

kotlinx.coroutinesリポジトリの中に詳しいドキュメントがあるので、それを読んでいく。 個人的にはコルーチンについて学ぶのに最初に読むドキュメントとしてはこれが一番わかりやすいと思う。

最初の例

最初の例として、launch 関数が使われた例が書かれている。

fun main(args: Array<String>) {
    launch { // 新しいコルーチンの起動。
            // スレッドを使う場合のスレッドの作成・開始に相当する。
        delay(1000L) // コルーチンの中断 (1 秒間)。
                // スレッドを使う場合の `Thread.sleep(1000L) 相当だが、
                // スレッドを使う場合と違って非ブロッキング。
        println("World!") // ここに処理が来るのは 1 秒間の中断の後。
    }
    println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。
    Thread.sleep(2000L) // アプリケーション全体が終了してしまうのを防ぐ。
}

スレッドとの対比を考えるとわかりやすいだろう。

最後に Thread.sleep しているのは、コメントにあるようにアプリケーション全体が終了してしまうことを防ぐためである。 コルーチンはデーモンスレッドのような感じあり、アクティブなコルーチンが存在していてもそれによってプロセスが生き続けるわけではない。

上記の例では main 関数全体はコルーチン上で動かされていない (ので Thread.sleep が使われている) が、全体をコルーチンで動かすために runBlocking 関数が紹介されている。 新しいコルーチンを起動し、コルーチンの処理が完了するまで現在のスレッドをブロックする、というもの。 コルーチンを使った処理とそうでない処理の橋渡しのために設計された関数である。

fun main(args: Array<String>) = runBlocking<Unit> {
    launch { /* ... この中は上の例と同じ ... */ }
    println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。
    delay(2000L) // アプリケーション全体が終了してしまうのを防ぐ。
}

上記の例では launch で起動したコルーチンを待つために delay(200) しているが、本来はコルーチン上の処理完了を明示的に待ちたい。 スレッドで Thread#join するのと同じように、launch の返り値である Jobjoin メソッドを呼ぶことで、コルーチンの処理が完了するのを (非ブロッキングに) 待つことができる。

fun main(args: Array<String>) = runBlocking<Unit> {
    val job = launch { /* ... この中は上の例と同じ ... */ }
    println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。
    job.join() // 上記コルーチンの処理完了を待つ。
}
Suspending 関数の導入

ここまでの例では全て suspending ラムダを使ってきたが、実際のコードでは関数やメソッドとして処理を記述したいことが多い。 この記事の最初の方で紹介した suspending 関数として記述できる。

fun main(args: Array<String>) = runBlocking {
    val job = launch { sayWorld() }
    println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。
    job.join()
}

suspend fun sayWorld() {
    delay(1000L) // コルーチンの中断 (1 秒間)。
    println("World!") // ここに処理が来るのは 1 秒間の中断の後。
}

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

Job#cancel メソッドを使ってコルーチンのキャンセル処理が可能。

キャンセル処理は協調的な処理である。 すなわち、コルーチンの処理がキャンセル処理に対応していなければならない。

具体的に言うと、下記のようなコードを書くとコルーチンの処理の中でキャンセルされるタイミングがないために、キャンセルがリクエストされてもコルーチンの処理が最後まで続いてしまう。

fun main(args: Array<String>) = runBlocking {
    val job = launch {
        sayWorld()
        println("Complete!")
            // `sayWorld` 処理中にキャンセルリクエストされても、
            // `sayWorld` がキャンセルに対応してないのでここも処理される。
    }
    println("Hello,")
    job.cancelAndJoin()
}

suspend fun sayWorld() {
    val startTimestamp = System.currentTimeMillis()
    while (System.currentTimeMillis() < startTimestamp + 5000L) {
        // Computing...
        // キャンセル処理に対応していないので、
        // キャンセルがリクエストされても 5 秒間動き続ける
    }
    println("World!")
            // キャンセルされるタイミングがないので、
            // キャンセルがリクエストされてもここも処理される。
}

キャンセルに対応する一つの方法としては、定期的にキャンセルに対応している suspending 関数を呼ぶことである。 例としては yield 関数が挙げられている。 他の方法として、自分でキャンセルされているかどうかを明示的に確認する、というものもある。

ちなみにキャンセルに対応した suspending 関数は、キャンセルされた際に CancellationException 例外を送出する。 そのため、try-finally による終了処理を書いておけば、キャンセル時にも終了処理が行われる。

ちなみにキャンセルされたコルーチンから suspending 関数を呼ぶと、(既にキャンセルされているので) CancellationException 例外が送出されてしまう。 なので、めったにないことではあるが、終了処理で suspending 関数を呼ぶことは (普通にやろうとしても) 不可能である。 この問題に対応するには、run 関数NonCancellable コンテキストを渡し、処理を実行してやる必要がある。

コルーチンをキャンセルしたい理由としてタイムアウト処理があるので、withTimeout 関数というものも用意されている。

async/await

TypeScript や ECMAScript を使っている人には async/await キーワードはなじみ深いものだと思う。 Kotlin でもライブラリで async 関数await メソッドといったものが提供されているが、コルーチンとの関係がいまいちよくわかっていなくて自分にとっては混乱のもとだった。

async 関数は、launch 関数と同じで新しいコルーチンを起動するものである。 launch 関数とは違って、返り値として Deferred が返される。 JS 界隈の人にとっては DeferredPromise というと馴染み深いであろう。 コルーチンから値を受け取ることができるのである。

そして、Deferred からの値の取り出しに使われるのが Deferred#await メソッドである。 この値の取り出しの待ち合わせも非ブロッキングである。

// 下記のようにコルーチン上で新しい非同期処理を開始して待合せたり
val deferredValue1 = async { /* 何らかの非同期処理 */ }
val deferredValue2 = async { /* 何らかの非同期処理 */ }
println("${deferredValue1.await()}, ${deferredValue2.await()}")

// 下記のように関数定義を行って、
fun asyncFoo() = async { /* 何らかの非同期処理 */ }
// コルーチンから使ったり、
val fooValue = asyncFoo().await()
// コルーチンの外で使ったりできる
val deferredFooValue = asyncFoo()
    // ただし await メソッドは suspending 関数なので
    // コルーチンの外では使えない

上のような使い方を見ると JS の async/await をより柔軟に使えるようにしたもの、というような印象を受けるが、実際はもう少しややこしい気がしている。 JS ではメインのイベントループが 1 つ回っているだけなので、async 関数の本体の処理と呼び出し側の処理は同じスレッド的なものの上で動く。 一方で、Kotlin の場合に JS のイベントループに相当するものがコルーチンだと考える *1 と、単純にコルーチン内で suspending 関数を呼び出すのが JS の async/await との対比になるような気がする。

つまり、TypeScript で以下のように書くのが、

// 下記のような関数を
async function asyncFoo(): string { /* ... */ }

// 下記のように使用する
let foo = await asyncFoo();

Kotlin における下記のコードに相当する、という考え方もできる。

// 下記のような suspending 関数を
suspend fun foo(): String = /* ... */

// コルーチン内で下記のように使用する
val foo = foo()

ともかく、JS 界隈の async/await との対比で理解しようとするよりは、コルーチンの仕組みをおさえた方が理解しやすいと感じた。

コルーチンコンテキストとディスパッチャ

  • コルーチンコンテキストとディスパッチャの詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
  • コルーチンは、CoroutineContext で表現されるコンテキストで実行される。
    • コルーチンコンテキストは、マップと集合によって値を持つコンテキスト要素 (CoroutineContext.Element) の (インデックス付きの) 集合である。
      • インデックス付きというのは、各要素を Key インスタンスによって参照可能であるということ。 例えばコルーチンコンテキストに含まれるジョブ要素を取得するには coroutineContext[Job] とすればよい。 (マップと言ってしまって良いと思われる。)
    • (CoroutineContext.ElementCoroutineContext を継承してるのは設計がイケてないという気がする……。)
  • コルーチンコンテキストはジョブの情報を持っているし、コルーチンディスパッチャ (CoroutineDispatcher) の情報も持っている。
    • coroutinContext[Job] って感じでコンテキストからジョブ情報を取れる。
    • コルーチンディスパッチャは、どのスレッド (またはスレッド群) でコルーチンが実行されるかを決めるもの。
  • launchasync のようなコルーチンビルダは、オプションでコルーチンコンテキストを受け取る。
    • デフォルトで使用されるディスパッチャは DefaultDispatcher で、現在の実装では CommonPool ディスパッチャと同じ。
    • 親のコルーチンと同じコンテキストを使いたい場合は coroutineContext で参照すればよい。
    • Unconfined ディスパッチャというものもあり、これはコルーチンを開始したり再開したりしたディスパッチャ上で動くもの。 つまり、コルーチンが中断して再開した場合、中断前と再開後で別のディスパッチャによって動かされる可能性がある。
  • コルーチン内でコンテキストを変化させることもできる : run 関数
  • コルーチンに親子関係を持たせたい場合は、親コルーチンのコンテキストを渡せばよい。 (コンテキストとしてジョブを指定してやればそれだけで良さそう。 詳細は下記)
    • コルーチンに親子関係があるとき、親コルーチンの終了は子のコルーチンの終了を待つし、親コルーチンがキャンセルされたときは子のコルーチンもキャンセルされる。
  • コンテキストは + 演算子で合成できる。 右側にあるコンテキストが左側のコンテキストの関係する箇所を置き換える。 (置き換えられなかったものは引き継がれる。)
  • デバッグの話
    • JVM オプションに -Dkotlinx.coroutines.debug を追加すると、スレッド名にコルーチン名が追加される。
    • CoroutineName コンテキスト要素を使うことで、コルーチンの名前を指定できる。
コルーチンの親子関係

上で説明したように、launch 関数などに指定するコンテキストにジョブが含まれていると、そのジョブが親コルーチンとなる。 そして、親コルーチンの終了は子のコルーチンが終了するのを待つ。

val job = launch {
    println("Parent coroutine")
    launch(coroutineContext) { // coroutineContext に親となるコルーチンのジョブが含まれている
        delay(1000L)
        println("Child coroutine")
    }
}
job.join()
// ここに処理が来る前に 「Child coroutine」 は出力される。

また、親のコルーチンがキャンセルされると子のコルーチンもキャンセルされることを応用して、Android の Activity などのライフサイクルに紐づけてコルーチンをキャンセルさせたい場合などに、ライフサイクルに紐づくジョブを作っておいて、それを親にするという手法を取ることができる。

// Activity のライフサイクルに紐づくジョブ
val activityRelatedJob = Job()

val networkJob = launch(activityRelatedJob) {
    // 非同期通信など
}

// Activity 終了時に親ジョブをキャンセルすることで子も全てキャンセルできる。
activityRelatedJob.cancel()

チャンネル (Channels)

  • チャンネルの詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
  • Deferred は単一の値をコルーチン間でやりとりする便利な方法。 値のストリームをコルーチン間でやり取りするのに使えるのがチャンネル。
  • ChannelBlockingQueue のようなもの。 主要な違いは値の追加と取り出しが suspending 関数になっていること。
  • それ以上値がないことを示すためにチャンネルを閉じることができる。 受け取り側は for ループで受け取ることができる。
  • Producer-consumer パターンとして一般的なパターン。
  • あるコルーチンが (おそらく無限に) 値のストリームを流し続けて、他のコルーチンが値を消費したり値に変換をかけたりするパイプラインというパターンもよく使われる。
  • バッファのないチャンネルの場合、送信が先に呼ばれると受信が呼ばれるまで送信処理が中断されるし、受信が先に呼ばれると送信が呼ばれるまで受信が中断される。
    • チャンネル作成時にバッファの大きさを指定できる。
  • 複数のコルーチンが受信や送信を呼んで中断している場合、先に呼んだものから順に値を受け取ったり送信したりできる。 (Channels are fair)

変更可能な状態の共有と並行性

  • 詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
  • コルーチンも複数スレッド上で動きうるので、並行性の問題がある。
  • Java でのマルチスレッド用の対策も 1 つの方法。
  • 特定の値を参照するのを特定のスレッドからだけにするのも 1 つの方法。 (Thread confinement)
    • 例えば UI に関するオブジェクトは UI スレッドからしか参照しないようにする、とか。
  • Mutex による排他制御という方法もある。
    • これはスレッドの世界における synchronizedReentrantLock 相当のもの。
    • Mutex は非ブロッキング
  • コルーチンと状態、そして他のコルーチンとやり取りするためのチャンネルをまとめたアクター (actor) という概念を用いても良い。
    • アクターを生成するための actor コルーチンビルダが用意されている。
    • 状態を触るのをアクターに限定することで並行性の問題を解消する。
おすすめ書籍

JVM 上での並行性・マルチスレッド対応については、下記の書籍がおすすめである。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

書評も書いたので参考にどうぞ。

Select expression

ドキュメントからの引用。

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}

どういう仕組みなのか初見ではわからなかったので簡単に解説しておく。

fizz.onReceive は、上で説明したように ReceiveChannel#receive に対応する select 文 (SelectClause1 オブジェクト) を返す。 そして、fizz.onReceive と後続のブロック部分は、省略せずに書くと fizz.onReceive.invoke({ ... }) という形式になっている。

SelectBuilderSelectClause1#invoke 拡張関数が定義されている。 select 関数が受け取る引数の定義が SelectBuilder.() -> Unit なので、fizz.onReceive.invoke({ ... }) というコードを記述できるのである。

詳細は別記事に書いた。

終わり

というわけで、Kotlin のコルーチンについて、下記のドキュメントを見ながら学んだことをまとめてみた。

この 2 つのドキュメントを読むことで、コルーチンについて基本的な部分はおおよそ理解できるだろう。 本記事が、皆さんがコルーチンを理解するための一助となれば。

その他参考になるページ

*1:それはそれで正しくなくて、より正確にはイベントループはコルーチンコンテキストの一種だと考えるのが対比としては一番良い気はする

Kotlin で拡張関数をオーバーライドして実装を切り替えられるぞ!

背景 : コルーチンの Select 式の実装を理解するのが難しかった

コルーチンのドキュメントを読んでいて select 関数というのが出てきたのだけど、これの実装がどうなっているのかすぐにはわからなかった。

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result 
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}
kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub

fizz.onReceiveSelectClause1<E>のプロパティなのだけど、その後ろのラムダが何なのかぱっと見はわからなかったのである。 (SelectClause1<E> 型には invoke は定義されていない。)

select 関数のシグネチャは以下。 引数の関数型はレシーバ付きで、レシーバの型は SelectBuilder である。

public inline suspend fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R

この SelectBuilder を見ると、下記のような拡張関数が定義されていた。 これらが fizz.onReceive { /* ... */ } の実体なのであった。

public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

しかも SelectBuilderインターフェイスで、拡張関数は別のクラスで実装されていた。

学び

拡張関数をオーバーライドできる

Extensions declared as members can be declared as open and overridden in subclasses.

Extensions - Kotlin Programming Language

拡張関数をメンバーとして定義できるのは知ってたのだけど、オーバーライドできるとは知らなかった!

interface StringExtensionScope {
    fun String.bar(): String
}

class StringExtensionScopeImpl : StringExtensionScope {
    override fun String.bar() = this + " bar"
}

そして実行時に実装を変更できる

拡張関数を定義した型をレシーバとするレシーバ付きの関数型を使うと、ラムダ内をスコープにして拡張関数を有効にできる! 実行時に実装を変更することも可能!

val printFooBar: StringExtensionScope.() -> Unit = {
    // StringExtensionScope で定義されている拡張関数を利用できる。
    println("foo".bar())
}

// そして実行時に拡張関数の実装を変更できる。
printFooBar(StringExtensionScopeImpl())

拡張関数といえば 「静的に解決されるものである」 という印象だったのでオーバーライドして実行時に実装を切り替えられるとは思っていなかったけど実際は切り替えることができる。 ちなみに拡張レシーバ (extension receiver) の型はやはり静的に見られるので、拡張レシーバの型に応じて動的に実装を切り替えるということはできない。

This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.

Extensions - Kotlin Programming Language

終わり

小ネタだけど、拡張関数は常に静的に解決されるものだと思ってたのでちょっとびっくりした。

Kotlin も拡張関数と operator とレシーバ付き関数型が組み合わさってくると結構コード追いづらくなるなーと感じる。 (まあ Scala とかと比べるとまだまだ追いやすい方だとは思うけど。)