ソース記事はこちら
コルーチンスコープは、異なるコルーチンの間の構造と親子関係の責務を担っている。新しいコルーチンは常にスコープの内側で開始する。コルーチンコンテキストには、コルーチンのカスタム名や、コルーチンがスケジュールされるスレッドを指定するディスパッチャーのような、与えられたコルーチンを実行するために使う追加の技術情報が格納されている。
launch
、async
、runBlocking
が新しいコルーチンを開始するために使われると、対応するスコープが作られる。これらすべての関数は、引数としてレシーバーを持つラムダを取り、暗黙的なレシーバ型はCoroutineScope
である。
launch { /* this: CoroutineScope */
}
新しいコルーチンは、スコープの内側でのみ開始することができる。launch
とasync
はCoroutineScope
に対する拡張として宣言されており、そのため呼び出すときは、暗黙あるいは明示的なレシーバーが常に渡されるはずである。runBlocking
によって開始されたコルーチンは例外で、runBlocking
はトップレベル関数として定義されている。しかしそれは現在のスレッドをブロックするため、主にmain関数や、テストの中で、仲立ちをする関数として使われることを目的としている。
runBlocking
やlaunch
やasync
の内側で新しいコルーチンを開始するとき、それはスコープの内側で開始する。
import kotlinx.coroutines.*
fun main() = runBlocking { /* this: CoroutineScope */
launch { /* ... */ }
// これと同じ
this.launch { /* ... */ }
}
runBlocking
の内側でlaunch
を呼ぶと、CoroutineScope
型の暗黙のレシーバーに対する拡張として呼び出している。あるいは明示的にthis.launch
と書くことができるだろう。
ネストされたコルーチン(この例ではlaunch
で始まる)は、外側のコルーチン(runBlocking
で始まる)の子であるといえる。親子の関係はスコープを通して動作する。つまり子のコルーチンは親のコルーチンに対応するスコープから開始する。
新しいコルーチンを開始することなく、新しいスコープを作ることは可能である。coroutineScope
関数はこれを行う。suspend
関数の内側、例えばloadContributorsConcurrent
の内側で、外側のスコープにアクセスすることなく、構造化された方法で新しいコルーチンを開始する必要があるとき、自動的にこのsuspend
関数が呼び出される元の外側のスコープの子となる、新しいコルーチンスコープを作ることができる。
GlobalScope.async
やGlobalScope.launch
を使うことで、グローバルスコープから新しいコルーチンを開始することも可能である。これはトップレベルの独立したコルーチンを作成する。
コルーチンの構造を提供するメカニズムは「構造化された並列性」と呼ばれる。構造化された並列性が持つグローバルスコープを超えるメリットを見ていこう。
- スコープは、一般的に子のコルーチンに対して責任があり、子の一生はスコープの一生に所属している。
- スコープは何かうまくいかなかったり、ユーザーが考えを変えて操作を取り消すことを決めた場合に、スコープは自動的に子のコルーチンをキャンセルできる。
- スコープは自動的に子のコルーチンの完了を待つ。したがってもしスコープがコルーチンと対応するなら、親のコルーチンはそのスコープで起動したすべてのコルーチンが完了するまで完了しない。
GlobalScope.async
を使うとき、いくつかのコルーチンをより小さなスコープに結びつける構造は存在しない。グローバルスコープから開始したコルーチンは、すべて独立している。つまりその一生はアプリケーション全体の一生によってのみ、限定されている。グローバルスコープから開始したコルーチンへの参照を保存し、その完了を待つか、明示的にキャンセルすることは可能だが、構造化されたコルーチンのように、自動的に発生されることはできない。
コントリビューターのロードのキャンセル
loadContributorsConcurrent
の2つのバージョンを比較しよう。ひとつはすべての子のコルーチンを開始するcoroutineScope
を使うものと、GloablScope
を使うもの。両方のバージョンで、親のコルーチンをキャンセルするときにどのようにふるまうかを比較しよう。
要求を送るすべてのコルーチンがに、3秒のディレイを追加する。そうすればコルーチンが開始したが、要求を送る前にロード処理をキャンセルする十分な時間ができる。
suspend fun loadContributorsConcurrent(service: GitHubService, req: RequestData): List<User> = coroutineScope {
// ...
async {
log("starting loading for ${repo.name}")
delay(3000)
// repoのコントリビューターをロードする
}
// ...
result // ※実際はresultではなくdeferreds.awaitAll().flatten().aggregate()
}
loadContributorsConcurrent
の実装を(Request5NotCancellable.kt
内の)loadContributorsNotCancellable
にコピーし、新しいcoroutineScope
の生成を削除する。async
呼び出しが解決に失敗するため、それらをGlobalScope.async
経由で開始する必要がある。
suspend fun loadContributorsNotCancellable(service: GitHubService, req: RequestData): List<User> { // #1
// ...
GlobalScope.async { // #2
log("starting loading for ${repo.name}")
delay(3000)
// repoのコントリビューターをロードする
}
// ...
return result // #3 ※実際はresultではなくdeferreds.awaitAll().flatten().aggregate()
}
この関数は、現在はラムダの内側の最後の式としてではなく、結果を直接返却している(#1
と#3
の行)。そして「コントリビューター」の全コルーチンはGlobalScope
の内側で開始している。(#2
の行)
プログラムを実行し、CONCURRENT
バージョン経由でコントリビューターのロードを選択することで開始することができ、「コントリビューター」の全コルーチンが開始するまで待つ必要があり、その後「キャンセル」をクリックする。ログを見ると、結果がログされていないので、すべて要求がまさにキャンセルされることがわかる。
2896 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - kotlin: loaded 40 repos
2901 [DefaultDispatcher-worker-2 @coroutine#4] INFO Contributors - starting loading for kotlin-koans
...
2909 [DefaultDispatcher-worker-5 @coroutine#36] INFO Contributors - starting loading for mpp-example
/* キャンセルをクリック */
/* 要求は送られていない */
それでは、同じ手順を、今度はNOT_CANCELLABLE
オプションを選んで繰り返そう。
2570 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - kotlin: loaded 30 repos
2579 [DefaultDispatcher-worker-1 @coroutine#4] INFO Contributors - starting loading for kotlin-koans
...
2586 [DefaultDispatcher-worker-6 @coroutine#36] INFO Contributors - starting loading for mpp-example
/* キャンセルをクリック */
/* しかし、すべての要求はまだ送られている */
6402 [DefaultDispatcher-worker-5 @coroutine#4] INFO Contributors - kotlin-koans: loaded 45 contributors
...
9555 [DefaultDispatcher-worker-8 @coroutine#36] INFO Contributors - mpp-example: loaded 8 contributors
何も起こらない!どのコルーチンもキャンセルされず、すべての要求は送られている。
キャンセル処理が「コントリビューター」のプログラムでどのように実装されているか、見てみよう。cancel
ボタンがクリックされると、メインの「ローディング」コルーチンを明示的にキャンセルする必要がある。その後、自動的に子のコルーチンがキャンセルされる。
それがボタンのクリックで「ローディング」コルーチンをキャンセルできる方法である。
interface Contributors {
fun loadContributors() {
// ...
when (getSelectedVariant()) {
CONCURRENT -> {
launch {
val users = loadContributorsConcurrent(req)
updateResults(users, startTime)
}.setUpCancellation() // #1
}
}
}
private fun Job.setUpCancellation() {
val loadingJob = this // #2
// キャンセルボタンがクリックされた場合は、ローディングジョブをキャンセルする
val listener = ActionListener {
loadingJob.cancel() // #3
updateLoadingStatus(CANCELED)
}
addCancelListener(listener)
// ローディングジョブが完了したら、ステータスを更新し、リスナーを削除する
}
}
launch
関数はJob
インスタンスを返却する。Job
には「ローディングコルーチン」への参照が格納されており、それはすべてのデータをロードし、結果を更新する。そのsetUpCancellation()
拡張関数を呼び出し(#1
の行)、レシーバーとしてJob
のインスタンスを渡している。これを表現する別の方法は、明示的に書くには次のようになる。
val job = launch { ... }
job.setUpCancellation()
読みやすさのために、setUpCancellation
関数の内部で、loadingJob
変数経由でレシーバーを参照している。(#2
の行)その後、cancel
ボタンにリスナーを追加し、そのため、それがクリックされると、loadingJob
がキャンセルされる。(#3
の行)
構造化された並列性により、親のコルーチンをキャンセルするだけで、自動的にすべての子のコルーチンにキャンセルが伝播される。
外側のスコープのコンテキストの使用
与えられたスコープの内部で新しいコルーチンが開始されると、そのすべては同じコンテキストで動作することを確認するのがはるかに簡単になる。もし必要であれば、コンテキストを置き換えるのもはるかに簡単である。
それでは前のセクションの末尾の質問に戻ろう。「外側のスコープからディスパッチャーを使うこと」は、まさにどのように動作するのか?(あるいは、より具体的には、「外側のスコープのコンテキストからディスパッチャーを使うこと」である)
新しいスコープは、coroutineScope
かコルーチンビルダーで作成される、常に外側のスコープのコンテキストを継承する。このケースだと、外側のスコープはsuspend loadContributorsConcurrent
の呼び出し元のスコープである。
launch(Dispatchers.Default) { // outer scope
val users = loadContributorsConcurrent(service, req)
// ...
}
すべてのネストされたコルーチンは、自動的に継承されたコンテキストで開始し、ディスパッチャーはそのコンテキストの一部である。それが、async
により開始されたすべてのコルーチンが、デフォルトディスパッチャーのコンテキストで開始する理由である。
suspend fun loadContributorsConcurrent(
service: GitHubService, req: RequestData
): List<User> = coroutineScope {
// このスコープは、外側のスコープからのコンテキストを継承している
// ...
async { // ネストされたコルーチンは、継承したコンテキストとともに開始する
// ...
}
// ...
}
構造化された並列性により、トップレベルのコルーチンを作るときに一度(ディスパッチャーのような)主なコンテキスト要素を指定することができる。すべてのネストされたコルーチンはその後、コンテキストを継承し、必要な場合のみそれを修正する。
留意すべきは、例えばAndroidのようなUIアプリケーションのためにコルーチンを使ってコードを書くとき、トップのコルーチン用にデフォルトで、CoroutineDispatchers.Main
を使用し、その後、異なるスレッドで動作することが必要な時に、異なるディスパッチャーを明示的に配置するのが、一般的な方法だということである。