visible true

技術的なメモを書く

Kotlin Coroutine 0.26.0 Structured concurrencyってなんなの

Structured concurrency – Roman Elizarov – Mediumでは、Kotlin coroutine 0.26.0でCoroutineScopeを導入した経緯などが書かれているんですが、タイトルのStructured concurrencyってのが結局なんなのか本文中にはあんまり出てこないんですよね。

んで、CoroutineScopeの動作の理解に精一杯で、coroutineScope関数がなんなのか、どういう時に使うべきなのかよくわからないし、ましてやsupervisorScope関数なんて言われた日には意味が不明なわけです。 とりあえずlaunch関数内でasync関数呼び出したら駄目なんだな〜ふ〜んなんで?

Notes on structured concurrency, or: Go statement considered harmfulを頑張って読む

Structured concurrency – Roman Elizarov – MediumのFurther readingにNotes on structured concurrency, or: Go statement considered harmful — njs blogを読むことを強く勧める!と書いています。パッと開くと超長いので心が折れかけるんですが読んでみたら面白かったのでメモします。

並行性の構造化

Structured concurrencyは概ね 並行性の構造化 って訳すとよさそうです。 構造化プログラミングはgotoを駆逐するために考えられたものですが、並行性の構造化は並行処理におけるgoto的なものを駆逐するための議論みたいです。 構造化プログラミングは順次, 反復, 分岐の構造を組み合わせてプログラム書くことでgotoは無くせるしむしろいい感じになるぜって話ですね。

並行性の中のgoto

エントリではgo statement(goroutineの起動)はgotoみたいなもんじゃ!って言ってます。go statementに限らず並行処理を起動するものはどれも同じみたいです。 たとえばtry-with-resources(これはJava)の中でスレッドを起動してその中でリソースにアクセスしたらどうなるか?ぶっ壊れます。 gotoじゃねえか!ということだそうです。でも確かにわかる気もする。

Trioの例

https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/#nurseries-a-structured-replacement-for-go-statements では、並行性についても構造化プログラミングと同じように並行性の構造のモデルを定義して取り入れるといいんじゃないか的な話と実際にpythontrioというライブラリの說明が始まります。

とりあえず図としてはこれがわかりやすいんじゃないかと思います。ほかのパターンもあるのかはちょっと分からないですがCoroutineScopeを理解するには十分かなと思います。

f:id:sys1yagi:20181212220527p:plain:w300

fork/joinに似てますけど実際に影響を受けてるらしいです。つまりどっから並行性が始まってどこで終わるんや、ってことを明示的に書けってことですね。これCoroutineScopeですね。こういう感じ。

suspend fun startParallel() = coroutineScope { // 並行性の分岐の根本
    // 3並行に分かれる
    val a = async { requestA() }
    val b = async { requestB() }
    val c = async { requestC() }

    // 合流する
    a.await() + b.await() + c.await()
}

タイミングがずれるケースとか複雑に分岐するケースとかもあるとおもうんですが、fork/joinの根本と合流がcoroutineScopeで適切に切られていればOKってことですね〜

例外の伝搬とCoroutineScope

例外の伝搬についてもfork/joinの図がわかりやすいですね。分岐の何処かが死んだら全体が即座に死ぬ。並行実行の単位がスコープってことですかね〜。 launch関数内でスコープを切らずにasync関数呼び出したら駄目なのは、async関数が並行性の分岐を表すからで、スコープを切らない場合はforkの根本がlaunch関数の親のスコープになるから、と說明できそうです(図を書いてみたもののカチッとしなかったので省略)。

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
// asyncがエラーになるとscopeがキャンセルされる
scope.launch {
  val a = async { requestA() }.await()
}

終わりに

とりあえず雑ですがメモです。大分わかってきました。まだ理解できてないとこもありそうですが.. ざっくりしか読んでないからまた何度か読みます。 Notes on structured concurrency, or: Go statement considered harmful — njs blog