Skip to content

コルーチンの基礎

このセクションでは、コルーチンの基本的な概念について説明します。

初めてのコルーチン

「コルーチン」は、中断可能な計算のインスタンスです。概念的にはスレッドと似ており、残りのコードと並行して動作するコードブロックを受け取ります。しかし、コルーチンは特定のどのスレッドにも束縛されません。あるスレッドで実行を中断し、別のスレッドで再開することができます。

コルーチンは軽量スレッドと考えることができますが、実際の使用方法をスレッドとは大きく異なるものにするいくつかの重要な違いがあります。

以下のコードを実行して、最初の動作するコルーチンを体験してみましょう。

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // 新しいコルーチンを起動し、続行します
        delay(1000L) // 1秒間(デフォルトの単位はミリ秒)非ブロッキングで待機します
        println("World!") // 待機後に表示
    }
    println("Hello") // メインコルーチンは前のコルーチンが遅延している間も続行します
}

完全なコードはこちらから取得できます。

以下の結果が表示されます。

text
Hello
World!

このコードが何をするのかを詳しく見ていきましょう。

launchは「コルーチンビルダー」です。これは残りのコードと並行して新しいコルーチンを起動し、残りのコードは独立して動作を続けます。そのため、最初にHelloが表示されました。

delayは特別な「中断関数」(suspending function)です。これはコルーチンを特定の時間「中断」します。コルーチンを中断しても、基盤となるスレッドは「ブロック」されず、他のコルーチンが実行され、基盤となるスレッドをそのコードに利用できるようになります。

runBlockingもコルーチンビルダーであり、通常のfun main()の非コルーチン世界と、runBlocking { ... }の波括弧内のコルーチンを持つコードを繋ぐものです。これは、IDEでrunBlockingの開始波括弧の直後に表示されるthis: CoroutineScopeというヒントによって強調表示されます。

このコードでrunBlockingを削除または忘れると、launchCoroutineScopeでのみ宣言されているため、launchの呼び出しでエラーが発生します。

Unresolved reference: launch

runBlockingの名前は、これを実行するスレッド(この場合はメインスレッド)が、runBlocking { ... }内のすべてのコルーチンが実行を完了するまで、呼び出しの間「ブロック」されることを意味します。スレッドは高価なリソースであり、それらをブロックすることは非効率的で、多くの場合望ましくないため、runBlockingがアプリケーションの最上位レベルでこのように使用されることはよくありますが、実際のコード内で使用されることはほとんどありません。

構造化された並行処理

コルーチンは、「構造化された並行処理」(structured concurrency)の原則に従います。これは、新しいコルーチンは、そのコルーチンのライフタイムを区切る特定のCoroutineScope内でしか起動できないことを意味します。上記の例では、runBlockingが対応するスコープを確立し、そのため前の例では1秒遅延した後にWorld!が表示されるまで待機し、その後終了することが示されています。

実際のアプリケーションでは、多くのコルーチンを起動することになります。構造化された並行処理は、コルーチンが失われたり、リークしたりしないことを保証します。外側のスコープは、すべての子コルーチンが完了するまで完了できません。また、構造化された並行処理は、コード内のエラーが適切に報告され、決して失われないことを保証します。

関数抽出によるリファクタリング

launch { ... }内のコードブロックを別の関数に抽出してみましょう。このコードに対して「関数抽出」のリファクタリングを実行すると、suspend修飾子を持つ新しい関数が得られます。これが最初の「中断関数」(suspending function)です。中断関数は、通常の関数と同様にコルーチン内で使用できますが、その追加機能として、他の suspending function(この例のdelayなど)を使用してコルーチンの実行を「中断」できる点があります。

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// これは最初の中断関数です
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

完全なコードはこちらから取得できます。

スコープビルダー

様々なビルダーによって提供されるコルーチンスコープに加えて、coroutineScopeビルダーを使用して独自のスコープを宣言することが可能です。これはコルーチンスコープを作成し、起動されたすべての子が完了するまで終了しません。

runBlockingcoroutineScopeビルダーは、どちらも自身の本体とすべての子が完了するのを待つため、似ているように見えるかもしれません。主な違いは、runBlockingメソッドが待機のために現在のスレッドを「ブロック」するのに対し、coroutineScopeは単に中断し、基盤となるスレッドを他の用途に解放する点です。この違いにより、runBlockingは通常の関数であり、coroutineScopeは中断関数です。

coroutineScopeは任意の suspending function から使用できます。例えば、HelloWorldの同時表示をsuspend fun doWorld()関数に移動させることができます。

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

完全なコードはこちらから取得できます。

このコードも以下のように表示されます。

text
Hello
World!

スコープビルダーと並行処理

coroutineScopeビルダーは、複数の並行操作を実行するために、任意の suspending function 内で使用できます。doWorld suspending function の内部で、2つの並行コルーチンを起動してみましょう。

kotlin
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が表示されます。doWorld内のcoroutineScopeは両方が完了した後にのみ完了するため、doWorldはその後で初めて戻り、Done文字列の表示を許可します。

text
Hello
World 1
World 2
Done

明示的なジョブ

launchコルーチンビルダーはJobオブジェクトを返します。これは起動されたコルーチンへのハンドルであり、その完了を明示的に待機するために使用できます。例えば、子コルーチンの完了を待ってから、「Done」という文字列を表示できます。

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch { // 新しいコルーチンを起動し、そのJobへの参照を保持
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join() // 子コルーチンが完了するまで待機
    println("Done") 
}

完全なコードはこちらから取得できます。

このコードは以下を生成します。

text
Hello
World!
Done

コルーチンは軽量

コルーチンはJVMスレッドよりもリソースを消費しません。スレッドを使用するとJVMの利用可能なメモリを使い果たしてしまうコードも、コルーチンを使用すればリソース制限に達することなく表現できます。例えば、以下のコードは50,000個の異なるコルーチンを起動し、それぞれが5秒待機してからピリオド('.')を表示しますが、消費するメモリはごくわずかです。

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(50_000) { // 多数のコルーチンを起動
        launch {
            delay(5000L)
            print(".")
        }
    }
}

完全なコードはこちらから取得できます。

もし同じプログラムをスレッドを使って書いた場合(runBlockingを削除し、launchthreadに、delayThread.sleepに置き換える)、それは大量のメモリを消費するでしょう。オペレーティングシステム、JDKのバージョン、およびその設定によっては、メモリ不足エラー(out-of-memory error)をスローするか、多数の並行実行スレッドが同時に存在しないようにスレッドの起動が遅くなるでしょう。