※ソース記事はこちら
この章では、基礎的なコルーチンの概要を範囲としている。
初めてのコルーチン
コルーチンは、中断可能な計算結果のインスタンスである。それはコードのブロックを、コードの残りと並列に動作させるという意味で、概念的にはスレッドに近い。しかし、コルーチンは特定のスレッドに結びついていない。あるスレッドで、実行を一時中断し、他のスレッドで再開することが可能である。
コルーチンは軽量なスレッドとして考えることができるが、現実の使い方において、スレッドとはかなり異なる、多くの重要な点が存在する。
初めての動作するコルーチンとして次のコードを実行してみる。
import kotlinx.coroutines.*
fun main() = runBlocking { // thisはCoroutineScope
launch { // 新しいコルーチンを起動して続行する
delay(1000L) // 1秒間ノンブロッキングな遅延 (デフォルトの時間単位はミリ秒)
println("World!") // 遅延の後で表示
}
println("Hello") // メインのコルーチンは、前のものが遅延している間、続行する
}
次のような結果が表示されるだろう。
Hello
World!
このコードが何をするのか、解剖していこう。
launchは、コルーチンビルダーである。これは残りのコードと並列に新しいコルーチンを起動し、独立して動作し続ける。それがHello
が最初に表示された理由である。
delayは特別なsuspend関数である。それは指定した時間、コルーチンを中断する。コルーチンを中断することは、その潜在的なスレッドをブロックせず、他のコルーチンでそのコードのために潜在的なスレッドを実行し、使うことができる。
runBlockingも、コルーチンビルダーであり、一般的なfun main()
のコルーチン外の世界と、runBlocking { ... }
の中カッコの内部のコルーチンのコードとの橋渡しをする。これは、runBlocking
の中カッコの開始の直後で、this: CoroutineScope
ヒントにより、IDEで強調表示される。
もし、コード内でrunBlocking
を削除あるいは、つけ忘れると、lauch呼び出しで、エラーが表示されるだろう。なぜなら、launch
はCoroutineScopeでのみ、宣言されるからである。
Unresolved reference: launch
runBlocking
の名前は、それを実行するスレッド(このケースではメインスレッド)が、runBlocking { ... }
の内側のすてのコルーチンが実行を完了するまで、呼び出しの間ブロックされるという意味である。runBlocking
がアプリケーションの最上位レベルで使われ、実際のコードの内部ではほとんど使われないのを度々見るだろうが、スレッドは高価なリソースであり、ブロッキングするのは非効率であり、大抵望ましくない。
構造化された並行性
コルーチンは、構造化された並列性の原則に従っており、それは、新しいコルーチンは、コルーチンの生存期間を決める、特定のCoroutineScopeでのみ起動する、という意味である。上の例では、runBlockingは、対応するスコープを確立していることを示しており、それが、前のサンプルが、1秒遅延した後で、World
が表示されるまで待機し、そのあとでのみ終了する理由である。
実際のアプリケーションでは、たくさんのコルーチンが起動されるだろう。構造化された並行性により、それらが失われたり、漏れたりしないことが保証される。外側のスコープは、すべての子のコルーチンが完了するまで完了することができない。構造化された並行性により、すべてのエラーは適切に報告され、決して失われないことも、保証されている。
関数を抽出リファクタリング
launch { ... }
の内部のコードブロックを分割した関数に抽出してみよう。このコードにおいて「関数を抽出」リファクタリングするときは、新しい関数suspend
修飾子をつける。これが初めてのsuspend関数である。suspend関数は、コルーチンの内側で、単に通常の関数のように使うことができるが、追加の機能として、今度はコルーチンの実行を中断するための(この例のdelay
のような)他のsuspend関数を使うことができる。
import kotlinx.coroutines.*
fun main() = runBlocking { // thisはCoroutineScope
launch { doWorld() }
println("Hello")
}
// これは最初のsuspend関数
suspend fun doWorld() {
delay(1000L)
println("World!")
}
スコープビルダー
異なるビルダーにより、提供されるコルーチンスコープに加え、CoroutineScopeを使う、独自のスコープを宣言することができる。それはコルーチンスコープを作り、すべての起動した子が完了するまで完了しない。
runBlockingとcoroutineScopeのビルダーは、両方とも本体とすべての子が完了するまで待つので、似ているように見えるかもしれない。主な違いは、runBlockingメソッドは待つための現在のスレッドを待つ一方で、coroutineScopeは、他が利用するために配下のスレッドを中断し、解放するだけである。この違いのため、runBlockingは通常の関数であり、coroutineScopeはsuspend関数である。
import kotlinx.coroutines.*
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // thisはCoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
このコードも次のような出力になる。
Hello
World!
スコープビルダーと並列性
coroutineScopeビルダーは、複数の並列操作を実行するため、内部にどんなsuspend関数を使うことができる。doWorld
suspend関数内部に二つの並列コルーチンを起動しよう。
import kotlinx.coroutines.*
// doWorld、続いて"Done"を順次実行する
fun main() = runBlocking {
doWorld()
println("Done")
}
// 並列で両方のセクションを実行する
suspend fun doWorld() = coroutineScope { // thisはCoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
launch { ... }
ブロック内部の両方のコードは、並列に実行され、開始から1秒後にWorld 1
が出力され、開始から2秒後にWorld 2
が出力される。
明示的なジョブ
launchコルーチンビルダーは、起動したコルーチンの取っ手であり、その完了を明示的に待つために使うことができる、Jobオブジェクトを返却する。例えば、子のコルーチンを待ち、その後"Done"文字列を出力することができる。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { // 新しいコルーチンを起動し、そのJobへの参照を保持する。
delay(1000L)
println("World!")
}
println("Hello")
job.join() // 子のコルーチンの完了を待つ
println("Done")
}
このコードは次の結果となる。
Hello
World!
Done
コルーチンは軽量
コルーチンはJVMのスレッドよりも、リソースがよりかからない。リソースの限界に到達せずに、コルーチンを使って、スレッドを使うことを表現することができるとき、JVMの利用可能なメモリを使い果たすようコーディングをしてみる。例えば、次のコードは10万回、明示的なコルーチンを起動し、それぞれが5秒待ちピリオド('.')を表示するが、メモリの消費は非常に少ない。
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // たくさんのコルーチンを起動する
launch {
delay(5000L)
print(".")
}
}
}
もしスレッドを使う同じプログラムを書く(runBlocking
を削除し、launch
をthread
に置換、delay
をThread.sleep
に置換)する場合、おそらく過度にメモリを消費し、out-of-memoryエラーをスローするだろう。