Spring MVCにおけるCoroutine（Kotlin）をきちんと理解する

Spring MVCでコルーチン（Kotlin）を使うこととなったため、調査しました。なかなか難しいなぁと思いました・・。


# Coroutineとは何か？
途中で中断できる「処理のまとまり」のことです。中断すると制御が他の処理に移りますが、処理（routine）同士が制御を互いに移し合う様から、接頭辞「Co」がついています。

# 中断（suspend）とは何か？
Kotlinにおける「中断(suspend)」というのは、かなり乱暴な表現をすると、ある処理を実行しているスレッドが、通信処理など「待ち」となるような処理を開始した後で、実際に待つことをせず、待っている間に別の処理を実行する、ということを指します。待ちが終わると、同じ or 別のスレッドが続きの処理を再開します。待ちの間にボケーっと待っているわけではなく、他の仕事をやるということですね。いわゆるノンブロッキングというやつです。

逆に、「中断」しない場合というのは、待ちが終わるまでスレッドも待ち状態となり、待ちが終わったら同じスレッドが続きの処理を実行します。待ちの間にボケーっと待っているわけです。いわゆるブロッキングというやつです。

# Spring MVCによるサーバサイド処理はこうなっている
[公式サイト](https://spring.pleiades.io/spring-framework/docs/current/reference/html/languages.html#coroutines)によると、以下の通り、Spring MVCではCoroutineがサポートされています。

>Spring MVC および WebFlux アノテーション付き @Controller での中断機能のサポート


では、具体的にはどのような仕組みになっているのでしょうか？

Spring MVCにおける処理シーケンスは以下の通りです。

* Spring側は、Webサーバからリクエストを受け付けると、コールバックを指定してMonoと呼ばれるものを生成します。Monoとは、Reactive Streamsという仕様に出てくる「Publisher」という役割を実装したものです。Publisher(Mono)の中では非同期に処理が実行され、Spring側はPublisherをsubscribeします。Publisherの処理が終わると、SubscriberであるSpring側に通知され、それを受けてSpring側は呼び出し元にレスポンスしていきます。
* このコールバックの中でコルーチンが起動され、コルーチン内でControllerのsuspend functionが実行されます。[コルーチン起動時にはDispatchers.Unconfinedが指定されています](https://github.com/spring-projects/spring-framework/blob/main/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt#L63)ので、Controllerのsuspend function内で起動する子コルーチンも、特段の指定がない限りは親コルーチンの設定であるDispatchers.Unconfinedを引き継ぎます。
* Controllerのsuspend functionでは、並列処理を行うためにasyncを呼び出し、子コルーチンを起動することが多いと思います。async呼び出しについては、以下の理解が大事と思います。
  * asyncは[定義](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)をみると分かるように、CoroutinScopeの拡張関数です。コルーチンの処理ブロックにはレシーバ（this）としてCoroutineScopeオブジェクトが設定されていますので、実際にはthis.async()と呼び出せます。thisは省略できるので、async()と直接呼び出せるのです。
  * 先述のとおり、asyncに特段の指定がない限り、子コルーチンは親コルーチンの設定であるDispatchers.Unconfinedを引き継ぎます。
  * Dispachers.DefaultやDispatchers.IOを指定するといった選択肢もあり、それぞれ高CPU負荷の処理やIO待ちの発生する処理に適していると一般的には言われます。しかし、実際には無闇に指定すべきではありません。それらに最適なスレッド数には限りがあり、それらが枯渇した時点でブロッキングが発生してしまうためです。詳しくは[こちら](https://www.techyourchance.com/coroutines-dispatchers-default-and-dispatchers-io-considered-harmful/)を参照ください。時期尚早な最適化はダメだよ、ということですね。
* asyncで起動した子コルーチンでは、親コルーチンを処理するスレッドがsuspendCoroutine or suspendCancellableCoroutineの呼び出しまでを実行し、続きの処理を別スレッドに任せます。自スレッドはasyncの戻り値であるDeferredを受け取り、async呼び出しの次の処理を実行していきます。DeferredはJobのサブクラスですから、つまるところJobの一種です。Deferredを操作することで、処理結果を受け取ったり、asyncで起動するコルーチンをキャンセルすっることができます。
* このようにControllerのスレッドはどんどんasyncでコルーチンを起動していき、どこかでそれらが終わるのをDeferred.await()で待ち、それらの処理結果を取得します。
* Controllerの処理が完了すると、Monoを介してSpring側に通知がいきます。先述した通り、通知を受けたSpring側は、呼び出し元にレスポンスしていきます（画面ならThymeleafなどに処理が移ります）。

# 何が嬉しいのか？

Spring MVCの場合、以下のメリットがあります。

* Spring側では、Contollerからの応答を待つためにスレッドがブロックされる、といったことは起こりません。Spring側はController側からのコールバックによる通知を受けてから、レスポンス処理にスレッドを回せば良いのです。Spring MVCとはいえ、Spring側からContollerを呼び出す部分だけはリアクティブな動きをするということです。
* Controller以降でasyncを呼び出す際は、親コルーチンを処理するスレッドがsuspendCoroutine or suspendCancellableCoroutineの呼び出しまでを実行し、続きの処理を別スレッドに任せるわけですから、Controllerを処理するメインともいえるスレッドはブロックされることなく、async以降の処理を継続できます。どんどんasyncで別コルーチンを起動していくことで、それらをシーケンシャルに呼び出すよりも明らかに性能が向上します。
* このような並列処理は、もちろんスレッドを多数起動することでも実現できますが、スレッドは1本あたり約2MBのメモリを消費するため、メモリ消費量が大変なことになります。一方、コルーチンでは各スレッドが上記のような「中断」を繰り返しながら細切れに処理をしていくため、少ないスレッドで同じことを実現できます。
* また、スレッドを多数起動した場合と比較したもう１つのメリットは、DBアクセスや通信処理などで「待ち」が発生する場合に、スレッドがブロックされず別の処理をできるため、CPUを余すところなく有効に活用できる、ということです。

# エラーハンドリング

子コルーチンは、親コルーチンから起動された非同期処理であり、両者はある意味「別世界」で実行されます。子コルーチンでエラーが起きても、親コルーチンの世界とは関係ないわけです。

子コルーチンでエラーが起きても、親コルーチンでリカバリして子コルーチンを正常終了させることはできません。

例えば・・・

* launchで起動された子コルーチンで例外が発生しても、親コルーチンに伝播されません。CoroutineContextに設定されたCoroutineExceptionHandlerが、エラー時のコールバックとして実行されるだけです（主にログ出力。なお、デフォルトのハンドラは何もしてくれない）。親コルーチンは例外にアクセスすることすらできません（job.join()してCancellationExceptionをキャッチしてcauseとかを見ればいけるのかもしれませんが）。
* asyncで起動された子コルーチンで例外が発生すると、親コルーチンに伝播されます（Deferrdをawaitした時にスローされる）。親コルーチンは子コルーチンで起きた例外を知ることができます。子コルーチンのCoroutineContextにCoroutineExceptionHandlerが設定されていても、ハンドラに通知されません（ハンドラは動きません）。

Spring MVCでは、launchよりもasyncを使うことが多いでしょう。asyncで発生する例外をハンドリングする必要性が無い、というケースがほとんどのはずです。その場合は以下の制御で問題ないでしょう。

asyncは後述するcoroutineScope()で作成したCoroutinScopeで動作しますが、async内で例外がスローされると、そのコルーチンは異常終了したと見なされ、Structured concurrencyの仕組みに沿って、同Scope内で起動していたコルーチンが全てキャンセルされます。キャンセルされたawaitは即処理が終了され、そのコルーチン内でCancellationExceptionがスローされます。（ただし、長い計算処理の中でキャンセル状態のチェックをしていない場合、すぐに終了されません）。async.await()でスローされた例外はcoroutineScope()の外側に伝播されます。

このあたりの実装イメージについては、以下をご覧ください。
https://github.com/nyandora/kotlin-sample/blob/master/src/main/kotlin/ErrorOnAsyncBlock.kt

coroutineScope()の外側に伝播された例外は、特段のハンドリングをされない限りは、Spring側まで伝播され、最終的にはいつものようにInterceptorなどがログ出力やらシステムエラー画面の表示やらをやります。

# コルーチンの各種APIを理解する
コルーチンに関連して色々なAPIがあり、初めはすごく混乱するのですが、これらを理解するためには「Structured concurrency」という概念を理解する必要があります。

コルーチンには生存期間があります。生存期間のことをCoroutineScopeと呼びます。変数にもスコープという生存期間がありますが、これと似たような概念です。

親コルーチンの生存期間の中に、子コルーチンの生存期間が包含されます。これは以下を意味します。

* 子コルーチンが完了して初めて、親コルーチンも完了することができます。
* 子コルーチンでエラー（CancellationExceptionを除く）が起こると、親コルーチンはキャンセルされ、他の子コルーチンもキャンセルされます。

すべてのコルーチンには生存期間、つまりCoroutineScopeがあるわけなので、コルーチンそのものより先に、CoroutineScopeが存在している必要があります。CoroutineScopeからコルーチンが生える感じです。CoroutineScopeの無いところから、いきなりコルーチンが開始されることは無いのです。このように、CoroutineScopeからコルーチンを生成されるため、launch/asyncといったコルーチンビルダーはCoroutineScopeの関数として定義されているのです。CoroutineScopeのインスタンスに対してlaunch/asyncしているわけです。

コルーチンを起動するときは、どのCoroutineScopeからコルーチンを生やし、どんな設定（CoroutineContext）で動かすか、を指定するわけですね。

この点を理解した上でAPIと向き合うと、APIについて腹落ちした理解ができてきます。

### [coroutineScope()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html)

* 親コルーチンのCoroutineScopeの中にネストされたCoroutineScopeを作り、ネストされたCoroutineScopeから子コルーチンを生やして処理を実行します。設定（CorutineContext）は親コルーチンのものを引き継ぎます。この子コルーチンの中で孫コルーチンを起動した場合、Structured concurrencyに従って適切にキャンセル／エラーハンドリングされます。例えば、子コルーチンのある処理で例外がスローされた場合、子コルーチンはキャンセルされ、孫コルーチンもキャンセルされた後、親コルーチンに（coroutineScope()の外側に）その例外がスローされます。
* 親コルーチンはcoroutineScope()の中の処理が完了しないと、先に進めません。
* Spring MVCでは、画面に表示するデータをAPIで他システムからかき集めてくる、といったケースがあると思います。その場合はasyncを呼び出しまくって並列で各APIを呼び出し、それらのAPIの結果が出そろうまで待つ、といったことがあります。asyncをどのCoroutineScopeに対して呼び出すのか指定する必要がありますので、coroutineScope()でスコープを作り出す必要があります。

### [withContext()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html)

* 現在のコルーチンを、設定（CoroutineContext）だけを変更して実行します。CoroutineScopeは現在のコルーチンが属しているものから変更されません。なので、withContextの引数blockに指定されるレシーバ（this）は、現在のコルーチンが属しているCoroutineScopeです。このCoroutineScopeからasyncとかでコルーチンを生やすわけですね。
* 現在のコルーチンはwithContext()の中の処理が完了しないと、先に進めません。
* 用途としては、パフォーマンスチューニングのためにDispatherを切り替える、くらいでしょうか・・？この用途を含め、Spring MVCにおいては出番がほぼ無いような気がします。

# おわりに
新たに分かったことがあれば追記していこうと思います。できれば図でのご説明も追加したいところですね・・。

間違っているところがあれば、是非是非ご指摘いただきたいです！