Kotlin コルーチン インフラストラクチャの維持と維持に役立つ、クリーンでシンプルな非同期コードを記述できます。 ネットワーク呼び出しなどの長時間実行タスクを管理しながら、アプリの応答性を高める ディスクオペレーションを実行できます
このトピックでは、Android のコルーチンについて詳しく説明します。コルーチンになじみがない場合は、このトピックを読む前にAndroid の Kotlin コルーチンを必ずお読みください。
長時間実行タスクを管理する
コルーチンは、長時間実行タスクを処理する 2 つのオペレーションを標準の関数に追加することによって構築されています。invoke
(または call
)と return
に加え、コルーチンは suspend
と resume
を追加します。
suspend
は、現在のコルーチンの実行を一時停止し、すべてのローカル変数を保存します。resume
は、中断されたコルーチンの実行を中断箇所から再開します。
suspend
関数は、他の suspend
関数から、または、launch
などのコルーチン ビルダーを使って新しいコルーチンを開始することによってのみ呼び出せます。
次に、長時間実行タスクを想定した場合のコルーチン実装のシンプルな例を示します。
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
上記の例では、get()
は引き続きメインスレッドで実行されますが、ネットワーク リクエストを開始する前にコルーチンを停止させます。ネットワーク リクエストが完了したら、get
は、コールバックを使用してメインスレッドに通知するのではなく、停止したコルーチンを再開します。
Kotlin は、スタック フレームを使用して、どの関数をローカル変数とともに実行するか管理します。コルーチンを停止すると、現在のスタック フレームがコピーされ、後で使用するために保存されます。再開すると、保存された場所からスタック フレームのコピーが戻されて、関数の実行が再開されます。コードは通常のシーケンシャル ブロック リクエストと同じように見えるかもしれませんが、コルーチンでは、ネットワーク リクエストによるメインスレッドのブロックが確実に回避されます。
メインセーフティでコルーチンを使用する
Kotlin のコルーチンは、ディスパッチャを使用して、コルーチンの実行に使用するスレッドを決定します。コードをメインスレッドの外部で実行するには、Kotlin コルーチンに対して、デフォルトまたは IO のいずれかのディスパッチャで処理を実行するよう指示できます。Kotlin では、すべてのコルーチンは、メインスレッド上で実行している場合であっても、ディスパッチャ内で実行する必要があります。コルーチンは自身を中断させることができ、ディスパッチャはそれを再開させる役目を担います。
コルーチンを実行すべき場所を指定するために、Kotlin では、デベロッパーが使用できる次の 3 つのディスパッチャを用意しています。
- Dispatchers.Main - このディスパッチャを使用すると、コルーチンはメインの Android スレッドで実行されます。UI を操作して処理を手早く作業する場合にのみ使用します。たとえば、
suspend
関数の呼び出し、Android UI フレームワーク オペレーションの実行、LiveData
オブジェクトのアップデートを行う場合などです。 - Dispatchers.IO - このディスパッチャは、メインスレッドの外部でディスクまたはネットワークの I/O を実行する場合に適しています。たとえば、Room コンポーネントの使用、ファイルの読み書き、ネットワーク オペレーションの実行などです。
- Dispatchers.Default - このディスパッチャは、メインスレッドの外部で CPU 負荷の高い作業を実行する場合に適しています。ユースケースの例としては、リストの並べ替えや JSON の解析などがあります。
前の例に続けて、ディスパッチャを使用して get
関数を再定義できます。get
の本文の中で、withContext(Dispatchers.IO)
を呼び出して、IO スレッドプールで実行するブロックを作成します。そのブロック内のコードはすべて、常に IO
ディスパッチャ経由で実行されます。withContext
自体が中断関数であるため、関数 get
も中断関数になります。
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
コルーチンによって、スレッドのディスパッチを細かく制御しながら実行できます。withContext()
ではコールバックを導入することなくコードの任意の行のスレッドプールを制御できるため、データベースからの読み取りやネットワーク リクエストといった極めて小さい関数に適用できます。withContext()
を使って、すべての関数をメインセーフにして、関数をメインスレッドから呼び出せるようにすることをおすすめします。そうすることで、呼び出し側において、関数を実行するためにどのスレッドを使用すべきか検討する必要がなくなります。
前の例では、fetchDocs()
はメインスレッドで実行されます。ただし、get
を安全に呼び出すことができ、それによりネットワーク リクエストはバックグラウンドで実行されます。
コルーチンは suspend
と resume
をサポートしているため、メインスレッド上のコルーチンは、withContext
のブロックが完了するとすぐに、get
の結果を使用して再開します。
withContext() のパフォーマンス
withContext()
同等のコールバック ベースの方法と比べて余分なオーバーヘッドが
説明します。さらに、状況によっては、コールバックをベースとする同等の実装以上に withContext()
呼び出しを最適化することが可能です。たとえば、関数がネットワークに対して 10 回の呼び出しを行う場合、外部の withContext()
を使用することによりスレッドを 1 回だけ切り替えるよう Kotlin に指示できます。このとき、ネットワーク ライブラリが withContext()
を複数回使用しても、それは同じディスパッチャにとどまり、スレッドの切り替えは回避されます。さらに、Kotlin では、Dispatchers.Default
と Dispatchers.IO
の切り替えの最適化により、スレッドの切り替えを可能な限り回避できます。
コルーチンを開始する
コルーチンは次の 2 つの方法のいずれかで開始できます。
launch
新しいコルーチンを開始し、呼び出し元に結果を返しません。「ファイア アンド フォーゲット」とみなされるあらゆる作業は、launch
を使用して開始できます。async
新しいコルーチンを開始し、suspend で結果を返すことができますawait
という関数を使用します。
一般に、標準の関数は await
を呼び出せないので、標準の関数から新規コルーチンを launch
する必要があります。async
は、別のコルーチン内部にいる場合、あるいは中断関数内部で並列分解を行う場合のみ、使用します。
並列分解
suspend
関数内で開始されたコルーチンはすべて、その関数が戻るときに停止する必要があるため、戻る前にそれらのコルーチンが確実に終了するようにしておく必要があります。Kotlin での構造化された同時実行では、1 つ以上のコルーチンを開始する coroutineScope
を定義できます。次に、await()
(コルーチンが 1 つの場合)または awaitAll()
(コルーチンが複数の場合)を使用して、関数から戻る前にこれらのコルーチンが終了することを保証できます。
例として、2 つのドキュメントを非同期的にフェッチする coroutineScope
を定義してみましょう。それぞれの遅延参照で await()
を呼び出すことにより、値が返される前に両方の async
オペレーションが終了することが保証されます。
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
次の例のように、コレクションで awaitAll()
を使用することもできます。
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
たとえ fetchTwoDocs()
が async
で新規コルーチンを起動しても、関数は awaitAll()
を使用して、起動されたコルーチンが、戻る前に終了するまで待ちます。ただし、awaitAll()
を呼び出していない場合でも、coroutineScope
ビルダーは、新規コルーチンがすべて完了するまでは、fetchTwoDocs
を呼び出したコルーチンを再開しません。
さらに、coroutineScope
が、コルーチンによってスローされた例外をキャッチし、呼び出し元に戻します。
並列分解の詳細については、中断関数の作成をご覧ください。
コルーチンの概念
CoroutineScope
CoroutineScope
launch
または async
を使用して、作成したコルーチンをすべて追跡します。実行中の作業(実行中のコルーチン)は、いつでも scope.cancel()
を呼び出してキャンセルできます。Android では、一部の KTX ライブラリが特定のライフサイクル クラスに独自の CoroutineScope
を提供しています。たとえば、ViewModel
には viewModelScope
、Lifecycle
には lifecycleScope
があります。ただし、ディスパッチャとは異なり、CoroutineScope
ではコルーチンは実行されません。
viewModelScope
は、コルーチンを使用した Android のバックグラウンド スレッドにある例でも使用されています。ただし、独自の CoroutineScope
を作成してアプリの特定のレイヤでコルーチンのライフサイクルを制御する必要がある場合は、次のように作成できます。
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
キャンセルされたスコープは、コルーチンをそれ以上作成できません。したがって、scope.cancel()
は、そのライフサイクルを制御するクラスが破棄されている場合にのみ呼び出す必要があります。viewModelScope
を使用すると、ViewModel
クラスは ViewModel の onCleared()
メソッドで自動的にスコープをキャンセルします。
ジョブ
Job
コルーチンへのハンドルです。launch
または async
で作成した各コルーチンは、コルーチンを一意に識別し、そのライフサイクルを管理する Job
インスタンスを返します。また、次の例に示すように、Job
を CoroutineScope
に渡してライフサイクルを詳細に管理することもできます。
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
CoroutineContext
は、次の要素のセットを使用してコルーチンの動作を定義します。
Job
: コルーチンのライフサイクルを制御します。CoroutineDispatcher
: 適切なスレッドに処理を送信します。CoroutineName
: コルーチンの名前。デバッグに役立ちます。CoroutineExceptionHandler
: キャッチされない例外を処理します。
スコープ内に作成された新規コルーチンの場合、新しい Job
インスタンスが新規コルーチンに割り当てられ、他の CoroutineContext
要素はそれを含むスコープから継承されます。継承された要素をオーバーライドするには、新しい CoroutineContext
を launch
ファンクションまたは async
ファンクションに渡します。なお、新規コルーチンには新しい Job
インスタンスが常に割り当てられるため、Job
を launch
または async
に渡しても効果はありません。
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
コルーチンに関する参考情報
コルーチンに関するその他の参考情報については、次のリンクをご覧ください。