コルーチンの基礎
このセクションでは、コルーチンの基本的な概念について説明します。
初めてのコルーチン
_コルーチン_は、中断可能な計算のインスタンスです。概念的にはスレッドと似ており、コードのブロックを受け取って、残りのコードと並行して実行します。しかし、コルーチンは特定のスレッドに拘束されません。あるスレッドで実行を中断し、別のスレッドで再開することができます。
コルーチンは軽量スレッドと考えることができますが、実際の使用においてはスレッドとは大きく異なる重要な点がいくつかあります。
以下のコードを実行して、初めて動作するコルーチンを試してみましょう。
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
NOTE
完全なコードはこちらで入手できます。
以下のような結果が表示されます。
Hello
World!
このコードが何をしているのかを分析してみましょう。
[launch
]は_コルーチンビルダー_です。これは新しいコルーチンを他のコードと並行して起動し、そのコードは独立して動作し続けます。そのため、「Hello」が最初に表示されたのです。
[delay
]は特別な_サスペンド関数_です。これはコルーチンを指定された時間_サスペンド_させます。コルーチンをサスペンドしても基盤となるスレッドを_ブロック_することはありませんが、他のコルーチンが実行され、その基盤スレッドを自身のコードのために使用することを可能にします。
[runBlocking
]もコルーチンビルダーであり、通常のfun main()
のような非コルーチン環境と、runBlocking { ... }
の波括弧内のコルーチンを含むコードとの橋渡しをします。これはIDEでrunBlocking
の開始波括弧の直後にあるthis: CoroutineScope
のヒントによって強調表示されます。
このコードでrunBlocking
を削除したり忘れたりすると、[launch
]呼び出しでエラーが発生します。これはlaunch
が[CoroutineScope
]でのみ宣言されているためです。
Unresolved reference: launch
runBlocking
という名前は、それを実行するスレッド(この場合はメインスレッド)が、runBlocking { ... }
内のすべてのコルーチンが実行を完了するまで、呼び出しの間_ブロック_されることを意味します。runBlocking
は、アプリケーションの最上位レベルでこのように使用されることがよくありますが、実際の実コード内で使用されることはほとんどありません。なぜなら、スレッドは高価なリソースであり、それをブロックすることは非効率的で、多くの場合望ましくないからです。
構造化された並行処理
コルーチンは構造化された並行処理の原則に従います。これは、新しいコルーチンが特定の[CoroutineScope
]内でのみ起動でき、そのスコープがコルーチンのライフタイムを区切ることを意味します。上記の例は、[runBlocking
]が対応するスコープを確立し、そのため前の例では1秒の遅延の後に「World!」が印刷されるまで待機し、その後で終了することを示しています。
実際のアプリケーションでは、多数のコルーチンを起動することになります。構造化された並行処理は、コルーチンが失われたりリークしたりしないことを保証します。外部のスコープは、そのすべての子コルーチンが完了するまで完了できません。構造化された並行処理は、コード内のエラーが適切に報告され、決して失われないことも保証します。
関数抽出のリファクタリング
launch { ... }
内のコードブロックを別の関数に抽出してみましょう。このコードで「関数抽出」のリファクタリングを実行すると、suspend
修飾子を持つ新しい関数が作成されます。これがあなたの最初の_サスペンド関数_です。サスペンド関数は通常の関数と同様にコルーチン内で使用できますが、その追加機能として、他のサスペンド関数(この例のdelay
など)を使用してコルーチンの実行を_サスペンド_できる点があります。
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
NOTE
完全なコードはこちらで入手できます。
スコープビルダー
さまざまなビルダーによって提供されるコルーチンスコープに加えて、[coroutineScope
][_coroutineScope
]ビルダーを使用して独自のスコープを宣言することが可能です。これはコルーチンスコープを作成し、起動されたすべての子が完了するまで完了しません。
[runBlocking
]と[coroutineScope
][_coroutineScope
]ビルダーは、どちらもその本体とすべての子の完了を待つため、似ているように見えるかもしれません。主な違いは、[runBlocking
]メソッドが待機のために現在のスレッドを_ブロック_するのに対し、[coroutineScope
][_coroutineScope
]は単にサスペンドするだけで、基盤となるスレッドを他の用途のために解放する点です。この違いから、[runBlocking
]は通常の関数であり、[coroutineScope
][_coroutineScope
]はサスペンド関数です。
任意のサスペンド関数からcoroutineScope
を使用できます。例えば、Hello
とWorld
の並行表示をsuspend fun doWorld()
関数に移動することができます。
import kotlinx.coroutines.*
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
NOTE
完全なコードはこちらで入手できます。
このコードも以下のように出力します。
Hello
World!
スコープビルダーと並行処理
[coroutineScope
][_coroutineScope
]ビルダーは、任意のサスペンド関数内で複数の並行操作を実行するために使用できます。doWorld
サスペンド関数内で2つの並行コルーチンを起動してみましょう。
import kotlinx.coroutines.*
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
NOTE
完全なコードはこちらで入手できます。
launch { ... }
ブロック内の両方のコードは_並行して_実行され、開始から1秒後に「World 1」が最初に表示され、次に開始から2秒後に「World 2」が表示されます。doWorld
内の[coroutineScope
][_coroutineScope
]は、両方が完了した後にのみ完了するため、doWorld
はその後で初めて戻り、「Done」文字列が表示されることを許可します。
Hello
World 1
World 2
Done
明示的なジョブ (Job)
[launch
]コルーチンビルダーは、起動されたコルーチンへのハンドルである[Job
]オブジェクトを返します。これは、その完了を明示的に待つために使用できます。例えば、子コルーチンの完了を待ってから、「Done」文字列を印刷することができます。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
}
NOTE
完全なコードはこちらで入手できます。
このコードは以下を出力します。
Hello
World!
Done
コルーチンは軽量である
コルーチンはJVMスレッドよりもリソース消費が少ないです。スレッドを使用するとJVMの利用可能なメモリを使い果たすようなコードでも、コルーチンを使用すればリソース制限に達することなく表現できます。例えば、以下のコードは50,000個の異なるコルーチンを起動し、それぞれが5秒間待機してからピリオド('.')を印刷しますが、消費するメモリは非常に少ないです。
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(50_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
NOTE
完全なコードはこちらで入手できます。
同じプログラムをスレッドを使用して記述した場合(runBlocking
を削除し、launch
をthread
に、delay
をThread.sleep
に置き換える)、大量のメモリを消費します。オペレーティングシステム、JDKのバージョン、およびその設定によっては、メモリ不足エラーがスローされるか、または同時に実行されるスレッドが多すぎないようにスレッドがゆっくりと起動します。