LoginSignup
5
7

More than 1 year has passed since last update.

コルーチンの基礎

Posted at

※ソース記事はこちら
この章では、基礎的なコルーチンの概要を範囲としている。

初めてのコルーチン

コルーチンは、中断可能な計算結果のインスタンスである。それはコードのブロックを、コードの残りと並列に動作させるという意味で、概念的にはスレッドに近い。しかし、コルーチンは特定のスレッドに結びついていない。あるスレッドで、実行を一時中断し、他のスレッドで再開することが可能である。
コルーチンは軽量なスレッドとして考えることができるが、現実の使い方において、スレッドとはかなり異なる、多くの重要な点が存在する。
初めての動作するコルーチンとして次のコードを実行してみる。

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呼び出しで、エラーが表示されるだろう。なぜなら、launchCoroutineScopeでのみ、宣言されるからである。

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を使う、独自のスコープを宣言することができる。それはコルーチンスコープを作り、すべての起動した子が完了するまで完了しない。
runBlockingcoroutineScopeのビルダーは、両方とも本体とすべての子が完了するまで待つので、似ているように見えるかもしれない。主な違いは、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関数を使うことができる。doWorldsuspend関数内部に二つの並列コルーチンを起動しよう。

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を削除し、launchthreadに置換、delayThread.sleepに置換)する場合、おそらく過度にメモリを消費し、out-of-memoryエラーをスローするだろう。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7