コルーチンの基礎
複数のタスクを同時に実行するアプリケーションを作成するために、並行処理(concurrency)として知られる概念で、Kotlinは_コルーチン_を使用します。コルーチンは中断可能な計算であり、明確で逐次的なスタイルで並行コードを記述できます。コルーチンは他のコルーチンと並行して、また場合によっては並列に実行できます。
JVMおよびKotlin/Nativeでは、コルーチンなどのすべての並行コードは、オペレーティングシステムによって管理される_スレッド_上で実行されます。コルーチンはスレッドをブロックする代わりに、実行を中断できます。これにより、あるコルーチンがデータの到着を待って中断し、別のコルーチンが同じスレッドで実行されることが可能になり、効果的なリソース利用が保証されます。
コルーチンとスレッドの違いについて詳しくは、コルーチンとJVMスレッドの比較をご覧ください。
中断関数
コルーチンの最も基本的な構成要素は_中断関数_です。これにより、実行中の操作はコードの構造に影響を与えることなく一時停止し、後で再開できます。
中断関数を宣言するには、suspendキーワードを使用します。
suspend fun greet() {
println("Hello world from a suspending function")
}中断関数は、別の中断関数からのみ呼び出すことができます。Kotlinアプリケーションのエントリポイントで中断関数を呼び出すには、main()関数にsuspendキーワードを付けます。
suspend fun main() {
showUserInfo()
}
suspend fun showUserInfo() {
println("Loading user...")
greet()
println("User: John Smith")
}
suspend fun greet() {
println("Hello world from a suspending function")
}この例ではまだ並行処理を使用していませんが、関数にsuspendキーワードを付けることで、他の中断関数を呼び出し、内部で並行コードを実行できるようになります。
suspendキーワードはKotlin言語のコア部分ですが、ほとんどのコルーチン機能はkotlinx.coroutinesライブラリを介して利用できます。
プロジェクトにkotlinx.coroutinesライブラリを追加する
kotlinx.coroutinesライブラリをプロジェクトに含めるには、ビルドツールに基づいて対応する依存関係設定を追加します。
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
}<!-- pom.xml -->
<project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.10.2</version>
</dependency>
</dependencies>
...
</project>初めてのコルーチンを作成する
このページの例では、コルーチンビルダー関数
CoroutineScope.launch()およびCoroutineScope.async()で明示的なthis式を使用しています。これらのコルーチンビルダーはCoroutineScopeの拡張関数であり、this式は現在のCoroutineScopeをレシーバーとして参照します。実用的な例については、コルーチンスコープからコルーチンビルダーを抽出するをご覧ください。
Kotlinでコルーチンを作成するには、次のものが必要です。
- 中断関数。
- 実行できるコルーチンスコープ。例えば、
withContext()関数内など。 - コルーチンを開始するための
CoroutineScope.launch()のようなコルーチンビルダー。 - どのスレッドを使用するかを制御するディスパッチャー。
マルチスレッド環境で複数のコルーチンを使用する例を見てみましょう。
kotlinx.coroutinesライブラリをインポートします。kotlinimport kotlinx.coroutines.*一時停止および再開できる関数に
suspendキーワードを付けます。kotlinsuspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") } suspend fun main() {}一部のプロジェクトでは
main()関数をsuspendとしてマークできますが、既存のコードと統合する場合やフレームワークを使用する場合は、それができない場合があります。その場合は、フレームワークのドキュメントで中断関数の呼び出しをサポートしているか確認してください。サポートされていない場合は、runBlocking()を使用して現在のスレッドをブロックすることで呼び出します。データフェッチやデータベースへの書き込みなどの中断タスクをシミュレートするために、
delay()関数を追加します。kotlinsuspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") delay(1000L) }
4. マルチスレッドの並行コードのエントリポイントを定義するために、[`withContext(Dispatchers.Default)`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html#)を使用して、共有スレッドプールで実行します。
```kotlin
suspend fun main() {
withContext(Dispatchers.Default) {
// Add the coroutine builders here
}
}
```
> 中断関数`withContext()`は通常[コンテキストスイッチ](coroutine-context-and-dispatchers.md#jumping-between-threads)に使用されますが、この例では並行コードの非ブロッキングなエントリポイントも定義します。これは[`Dispatchers.Default`ディスパッチャー](#coroutine-dispatchers)を使用して、マルチスレッド実行のための共有スレッドプールでコードを実行します。デフォルトでは、このプールはランタイムで利用可能なCPUコアと同じ数のスレッド(最低2つ)を使用します。
>
> `withContext()`ブロック内で起動されたコルーチンは、同じコルーチンスコープを共有し、[構造化された並行処理](#coroutine-scope-and-structured-concurrency)を保証します。
>
{style="note"}
5. [`CoroutineScope.launch()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html)のような[コルーチンビルダー関数](#coroutine-builder-functions)を使用してコルーチンを開始します。
```kotlin
suspend fun main() {
withContext(Dispatchers.Default) { // this: CoroutineScope
// このスコープ内でCoroutineScope.launch()を使ってコルーチンを開始します
this.launch { greet() }
println("The withContext() on the thread: ${Thread.currentThread().name}")
}
}
```
6. これらの要素を組み合わせて、共有スレッドプール上で複数のコルーチンを同時に実行します。
```kotlin
// コルーチンライブラリをインポートします
import kotlinx.coroutines.*
// kotlin.time.Durationをインポートして秒単位で期間を表現します
import kotlin.time.Duration.Companion.seconds
// 中断関数を定義します
suspend fun greet() {
println("The greet() on the thread: ${Thread.currentThread().name}")
// 1秒間中断し、スレッドを解放します
delay(1.seconds)
// ここではdelay()関数が中断API呼び出しをシミュレートします
// ネットワークリクエストのような中断API呼び出しをここに追加できます
}
suspend fun main() {
// このブロック内のコードを共有スレッドプールで実行します
withContext(Dispatchers.Default) { // this: CoroutineScope
this.launch() {
greet()
}
// 別のコルーチンを開始します
this.launch() {
println("The CoroutineScope.launch() on the thread: ${Thread.currentThread().name}")
delay(1.seconds)
// ここではdelay関数が中断API呼び出しをシミュレートします
// ネットワークリクエストのような中断API呼び出しをここに追加できます
}
println("The withContext() on the thread: ${Thread.currentThread().name}")
}
}
```
この例を複数回実行してみてください。プログラムを実行するたびに出力順序とスレッド名が変更される可能性があります。これは、OSがスレッドの実行を決定するためです。
> 追加情報として、コードの出力でコルーチン名をスレッド名の横に表示できます。これを行うには、ビルドツールまたはIDEの実行構成で`-Dkotlinx.coroutines.debug` VMオプションを渡します。
>
> 詳しくは、[コルーチンのデバッグ](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/topics/debugging.md)をご覧ください。
>
{style="tip"}
## コルーチンスコープと構造化された並行処理
アプリケーションで多くのコルーチンを実行する場合、それらをグループとして管理する方法が必要です。Kotlinコルーチンは、この構造を提供するために_構造化された並行処理_と呼ばれる原則に依存しています。
この原則によれば、コルーチンは、ライフサイクルがリンクされた親タスクと子タスクのツリー階層を形成します。コルーチンのライフサイクルは、作成から完了、失敗、またはキャンセルまでの状態のシーケンスです。
親コルーチンは、すべての子コルーチンが完了するまで終了しません。親コルーチンが失敗したりキャンセルされたりした場合、すべての子コルーチンも再帰的にキャンセルされます。このようにコルーチンを接続することで、キャンセルとエラー処理が予測可能で安全になります。
構造化された並行処理を維持するために、新しいコルーチンは、ライフサイクルを定義および管理する[`CoroutineScope`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/)内でしか起動できません。`CoroutineScope`には、ディスパッチャーやその他の実行プロパティを定義する_コルーチンコンテキスト_が含まれています。あるコルーチン内で別のコルーチンを開始すると、それは自動的に親スコープの子になります。
`CoroutineScope`上で`CoroutineScope.launch()`などの[コルーチンビルダー関数](#coroutine-builder-functions)を呼び出すと、そのスコープに関連付けられたコルーチンの子コルーチンが開始されます。ビルダーのブロック内では、[レシーバー](lambdas.md#function-literals-with-receiver)はネストされた`CoroutineScope`であるため、そこで起動するコルーチンはすべてその子となります。
### `coroutineScope()`関数でコルーチンスコープを作成する
現在のコルーチンコンテキストで新しいコルーチンスコープを作成するには、[`coroutineScope()`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html)関数を使用します。この関数は、コルーチンサブツリーのルートコルーチンを作成します。これは、ブロック内で起動されたコルーチンの直接の親であり、それらが起動する任意のコルーチンの間接的な親です。`coroutineScope()`は中断ブロックを実行し、そのブロックとそこで起動されたすべてのコルーチンが完了するまで待機します。
例を次に示します。
```kotlin
// kotlin.time.Durationをインポートして秒単位で期間を表現します
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
// コルーチンコンテキストでディスパッチャーが指定されていない場合、
// CoroutineScope.launch()はDispatchers.Defaultを使用します
suspend fun main() {
// コルーチンサブツリーのルート
coroutineScope { // this: CoroutineScope
this.launch {
this.launch {
delay(2.seconds)
println("Child of the enclosing coroutine completed")
}
println("Child coroutine 1 completed")
}
this.launch {
delay(1.seconds)
println("Child coroutine 2 completed")
}
}
// coroutineScope内のすべての子が完了した後にのみ実行されます
println("Coroutine scope completed")
}この例ではディスパッチャーが指定されていないため、coroutineScope()ブロック内のCoroutineScope.launch()ビルダー関数は現在のコンテキストを継承します。そのコンテキストに指定されたディスパッチャーがない場合、CoroutineScope.launch()はDispatchers.Defaultを使用します。これは共有スレッドプールで実行されます。
コルーチンスコープからコルーチンビルダーを抽出する
場合によっては、CoroutineScope.launch()のようなコルーチンビルダーの呼び出しを別の関数に抽出したいと思うかもしれません。
次の例を考えてみましょう。
suspend fun main() {
coroutineScope { // this: CoroutineScope
// CoroutineScopeがレシーバーであるCoroutineScope.launch()を呼び出す
this.launch { println("1") }
this.launch { println("2") }
}
}
this.launchは、明示的なthis式なしでlaunchと記述することもできます。これらの例では、CoroutineScopeの拡張関数であることを強調するために、明示的なthis式を使用しています。Kotlinでのレシーバーを持つラムダの動作について詳しくは、レシーバーを持つ関数リテラルをご覧ください。
coroutineScope()関数は、CoroutineScopeレシーバーを持つラムダを受け取ります。このラムダ内では、暗黙的なレシーバーはCoroutineScopeであるため、CoroutineScope.launch()やCoroutineScope.async()などのビルダー関数は、そのレシーバー上の拡張関数として解決されます。
コルーチンビルダーを別の関数に抽出するには、その関数がCoroutineScopeレシーバーを宣言する必要があります。そうしないと、コンパイルエラーが発生します。
import kotlinx.coroutines.*
suspend fun main() {
coroutineScope {
launchAll()
}
}
fun CoroutineScope.launchAll() { // this: CoroutineScope
// CoroutineScope上で.launch()を呼び出す
this.launch { println("1") }
this.launch { println("2") }
}
/* -- CoroutineScopeをレシーバーとして宣言せずにlaunchを呼び出すとコンパイルエラーになります --
fun launchAll() {
// コンパイルエラー: thisが定義されていません
this.launch { println("1") }
this.launch { println("2") }
}
*/コルーチンビルダー関数
コルーチンビルダー関数は、実行するコルーチンを定義するsuspendラムダを受け入れる関数です。以下にいくつかの例を示します。
コルーチンビルダー関数は、実行するCoroutineScopeが必要です。これは既存のスコープ、またはcoroutineScope()、runBlocking()、withContext()などのヘルパー関数で作成するスコープのいずれかです。各ビルダーは、コルーチンがどのように開始され、その結果とどのようにやり取りするかを定義します。
CoroutineScope.launch()
CoroutineScope.launch()コルーチンビルダー関数は、CoroutineScopeの拡張関数です。これは、既存のコルーチンスコープ内で、スコープの残りをブロックすることなく、新しいコルーチンを開始します。
結果が必要ない場合や待機したくない場合に、他の作業と並行してタスクを実行するためにCoroutineScope.launch()を使用します。
// kotlin.time.Durationをインポートしてミリ秒単位で期間を表現できるようにします
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.*
suspend fun main() {
withContext(Dispatchers.Default) {
performBackgroundWork()
}
}
suspend fun performBackgroundWork() = coroutineScope { // this: CoroutineScope
// スコープをブロックせずに実行されるコルーチンを開始
this.launch {
// バックグラウンド作業をシミュレートするために中断
delay(100.milliseconds)
println("Sending notification in background")
}
// 前のコルーチンが中断している間もメインコルーチンは続行
println("Scope continues")
}この例を実行すると、main()関数がCoroutineScope.launch()によってブロックされず、コルーチンがバックグラウンドで動作している間も他のコードを実行し続けることがわかります。
CoroutineScope.launch()関数は、Jobハンドルを返します。このハンドルを使用して、起動されたコルーチンが完了するのを待機します。 詳しくは、キャンセルとタイムアウトをご覧ください。
CoroutineScope.async()
CoroutineScope.async()コルーチンビルダー関数は、CoroutineScopeの拡張関数です。これは、既存のコルーチンスコープ内で並行計算を開始し、最終的な結果を表すDeferredハンドルを返します。結果が準備できるまでコードを中断するために、.await()関数を使用します。
// kotlin.time.Durationをインポートしてミリ秒単位で期間を表現できるようにします
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.*
suspend fun main() = withContext(Dispatchers.Default) { // this: CoroutineScope
// 最初のページのダウンロードを開始
val firstPage = this.async {
delay(50.milliseconds)
"First page"
}
// 2番目のページを並列でダウンロード開始
val secondPage = this.async {
delay(100.milliseconds)
"Second page"
}
// 両方の結果を待機し、比較
val pagesAreEqual = firstPage.await() == secondPage.await()
println("Pages are equal: $pagesAreEqual")
}runBlocking()
runBlocking()コルーチンビルダー関数はコルーチンスコープを作成し、そのスコープで起動されたコルーチンが終了するまで現在のスレッドをブロックします。
runBlocking()は、非中断コードから中断コードを呼び出すための他の選択肢がない場合にのみ使用します。
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.*
// 変更できないサードパーティインターフェース
interface Repository {
fun readItem(): Int
}
object MyRepository : Repository {
override fun readItem(): Int {
// 中断関数への橋渡し
return runBlocking {
myReadItem()
}
}
}
suspend fun myReadItem(): Int {
delay(100.milliseconds)
return 4
}コルーチンディスパッチャー
_コルーチンディスパッチャー_は、コルーチンが実行に使用するスレッドまたはスレッドプールを制御します。コルーチンは常に単一のスレッドに縛られているわけではありません。ディスパッチャーに応じて、あるスレッドで一時停止し、別のスレッドで再開できます。これにより、すべてのコルーチンに個別のスレッドを割り当てることなく、多くのコルーチンを同時に実行できます。
コルーチンは異なるスレッドで中断および再開できますが、コルーチンが中断する前に書き込まれた値は、再開時に同じコルーチン内で引き続き利用できることが保証されます。
ディスパッチャーは、コルーチンスコープと連携して、コルーチンがいつ、どこで実行されるかを定義します。コルーチンスコープがコルーチンのライフサイクルを制御する一方、ディスパッチャーはどのスレッドが実行に使用されるかを制御します。
すべてのコルーチンにディスパッチャーを指定する必要はありません。デフォルトでは、コルーチンは親スコープからディスパッチャーを継承します。異なるコンテキストでコルーチンを実行するためにディスパッチャーを指定できます。
コルーチンコンテキストにディスパッチャーが含まれていない場合、コルーチンビルダーは
Dispatchers.Defaultを使用します。
kotlinx.coroutinesライブラリには、さまざまなユースケースに対応する異なるディスパッチャーが含まれています。例えば、Dispatchers.Defaultは、メインスレッドとは別に、バックグラウンドで作業を実行する共有スレッドプールでコルーチンを実行します。これは、データ処理のようなCPU集中型操作にとって理想的な選択肢となります。
CoroutineScope.launch()のようなコルーチンビルダーにディスパッチャーを指定するには、引数として渡します。
suspend fun runWithDispatcher() = coroutineScope { // this: CoroutineScope
this.launch(Dispatchers.Default) {
println("Running on ${Thread.currentThread().name}")
}
}あるいは、withContext()ブロックを使用して、その中のすべてのコードを指定されたディスパッチャーで実行することもできます。
// kotlin.time.Durationをインポートしてミリ秒単位で期間を表現できるようにします
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.*
suspend fun main() = withContext(Dispatchers.Default) { // this: CoroutineScope
println("Running withContext block on ${Thread.currentThread().name}")
val one = this.async {
println("First calculation starting on ${Thread.currentThread().name}")
val sum = (1L..500_000L).sum()
delay(200L)
println("First calculation done on ${Thread.currentThread().name}")
sum
}
val two = this.async {
println("Second calculation starting on ${Thread.currentThread().name}")
val sum = (500_001L..1_000_000L).sum()
println("Second calculation done on ${Thread.currentThread().name}")
sum
}
// 両方の計算を待機し、結果を出力
println("Combined total: ${one.await() + two.await()}")
}コルーチンディスパッチャーとその用途について、Dispatchers.IOやDispatchers.Mainなどの他のディスパッチャーを含め、さらに詳しく学ぶには、コルーチンコンテキストとディスパッチャーをご覧ください。
コルーチンとJVMスレッドの比較
コルーチンはJVM上のスレッドのようにコードを並行して実行する中断可能な計算ですが、内部的には異なる動作をします。
_スレッド_はオペレーティングシステムによって管理されます。スレッドは複数のCPUコアでタスクを並列に実行でき、JVMにおける並行処理の標準的なアプローチを表します。スレッドを作成すると、オペレーティングシステムはスタック用のメモリを割り当て、カーネルを使用してスレッド間で切り替えます。これにより、スレッドは強力ですが、リソースを大量に消費します。各スレッドは通常数メガバイトのメモリを必要とし、通常JVMは一度に数千のスレッドしか処理できません。
一方、コルーチンは特定のスレッドに縛られません。あるスレッドで中断し、別のスレッドで再開できるため、多くのコルーチンが同じスレッドプールを共有できます。コルーチンが中断すると、スレッドはブロックされず、他のタスクを実行するために解放されたままになります。これにより、コルーチンはスレッドよりもはるかに軽量であり、システムリソースを枯渇させることなく、1つのプロセスで何百万ものコルーチンを実行できます。
50,000個のコルーチンがそれぞれ5秒待機し、その後ピリオド(.)を出力する例を見てみましょう。
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
suspend fun main() {
withContext(Dispatchers.Default) {
// それぞれが5秒待機し、その後ピリオドを出力する50,000個のコルーチンを起動
printPeriods()
}
}
suspend fun printPeriods() = coroutineScope { // this: CoroutineScope
// それぞれが5秒待機し、その後ピリオドを出力する50,000個のコルーチンを起動
repeat(50_000) {
this.launch {
delay(5.seconds)
print(".")
}
}
}次に、JVMスレッドを使用した同じ例を見てみましょう。
import kotlin.concurrent.thread
fun main() {
repeat(50_000) {
thread {
Thread.sleep(5000L)
print(".")
}
}
}このバージョンを実行すると、各スレッドが独自のメモリスタックを必要とするため、はるかに多くのメモリを使用します。50,000個のスレッドの場合、これは最大100GBになる可能性がありますが、同じ数のコルーチンでは約500MBです。
オペレーティングシステム、JDKバージョン、および設定によっては、JVMスレッド版がメモリ不足エラーをスローしたり、一度に多数のスレッドが実行されないようにスレッドの作成を遅らせたりする可能性があります。
次のステップ
- 中断関数の構成で、中断関数の組み合わせについて詳しく学びましょう。
- キャンセルとタイムアウトで、コルーチンをキャンセルし、タイムアウトを処理する方法を学びましょう。
- コルーチンコンテキストとディスパッチャーで、コルーチンの実行とスレッド管理についてさらに深く掘り下げましょう。
- 非同期フローで、複数の非同期に計算された値を返す方法を学びましょう。
