405
296

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Kotlin の Coroutine を概観する

Last updated at Posted at 2017-11-17

Kotlin では バージョン 1.1 から Coroutine が導入されています。

検索すると coroutine, async/await に関する情報がたくさん見つかりますが、そもそも coroutine がどのようなもので、どのように使うのかを簡単に説明した資料が少なかったため、 Kotlin 公式ドキュメントを元に勉強してみました。

筆者は Coroutine 自体に馴染みがなく、今回 Kotlin で初めて触れています。根本的な認識の誤り等なにかございましたらご指摘いただければ嬉しく思います。

※ 2017年11月現在の最新版は 1.1.60 ですが、 Coroutine は未だ experimental となっています。そのため今後破壊的な変更が行われる可能性があります。

Coroutine は軽量な Thread

Coroutine とは、つまるところ軽量な Thread のようなものです。
Thread のように他の処理をブロックすることなく並列に処理を行えます。
しかしとても軽量です。
Thread はその開始と終了に無視できないコストがかかるのに対して、 Coroutine はほとんど気にする必要のない程度のコストしかかかりません。
同時に数千数万の Coroutine を容易に実行することができます。

launch

Coroutine を作成して利用するのはとても簡単です。

launch {
    // 何らかの処理
}

これだけでラムダ内の何らかの処理をその実行スレッドをブロックすることなく並列に処理することができます。

次のプログラムを実行するとどうなるでしょうか?

fun main(args: Array<String>) {
    println("start")
    launch {
        println("coroutine!")
    }
    println("end")
}

実はこれでは次のように出力されてしまいます。

start
end

launch 内の処理が実行されていません。
なぜなら、 launch はその実行スレッドを全くブロックしないからです。
"start" が出力され、 launch が実行された直後にすぐに "end" が出力されてプログラムは終了します。
launch のラムダが実行されるよりも先にプログラムが終了しているのです。

fun main(args: Array<String>) {
    println("start")
    launch {
        println("coroutine!")
    }
    Thread.sleep(1000)
    println("end")
}

このようにすると、期待通り次のように出力されます。

start
coroutine!
end

launch のラムダ内の処理が完了するまでの十分な時間だけスレッドを停止させてから、 "end" と出力して終了するためです。

runBlocking

runBlocking はその名の通り、現在のスレッドをブロックします。

先程のプログラムを以下のように変更します。

fun main(args: Array<String>) {
    println("start")
    runBlocking {
        println("coroutine!")
    }
    println("end")
}

実行すると以下のように出力されます。

start
coroutine!
end

Corotuine が終了するまで待ってから、 "end" が出力されることがわかります。

また、 runBlocking は実行した Coroutine の戻り値を取得することもできます。

次のように変更します。

fun main(args: Array<String>) {
    println("start")
    val text = runBlocking {
        "coroutine!"
    }
    println(text)
    println("end")
}

この場合も先程と同じく、"start", "coroutine!", "end" の順に出力されます。

ちなみに runBlocking は Coroutine 内から呼び出すべきではありません。
従来型のブロッキング処理と Coroutine で実装された処理の橋渡しとしてや、例のように main 関数内での使用、またはテストでの使用などを想定して設計されているためです。

Job#join()

runBlocking は現在のスレッドをブロックするため、コルーチンとスレッドの境界でしか使用すべきではありません。
コルーチン内で他のコルーチンの終了を待機するために Job#join が利用できます。
launch は戻り値に Job インスタンスを返すため、上の例は次のように変更できます。

fun main(args: Array<String>) = runBlocking() {
    println("start")
    launch {
        println("coroutine!")
    }.join()
    println("end")
}

async/await

runBlocking を使用すれば、 Coroutine の戻り値を取得することができました。
しかし、現在のスレッドをブロックして Coroutine の戻り値を得られるというのは非同期処理を行いたい場合はあまり嬉しくありません。
launch {}.join() のように他のコルーチンを非ブロッキングで中断し、かつ runBlocking のように終了時に戻り値を受け取りたい場合はどうすればよいでしょうか。

async と Deferred<T>

そこで async の出番です。
async は Coroutine を作成し、戻り値として Deferred<T> 型のオブジェクトを返します。
Deferred<T> 型は future のようなもので、 Coroutine の処理が終わったタイミングでその戻り値を取得することができます。

まず、先程からの例を次のように変更します。

fun main(args: Array<String>) {
    println("start")
    async {
        println("coroutine!")
    }
    println("end")
}

しかしこれでは "coroutine!" は出力されません。 launch の時と同じく Coroutine が実行され "coroutine!" が出力されるよりも先にプログラムが終了しているためです。

await

やはり Coroutine の終了を待つ必要があります。
async の返す Deferred<T> インタフェースには await メソッドが定義されています。
await メソッドは、 async が起動したコルーチンの終了まで現在のコルーチンを中断し、終了したコルーチンの戻り値を取得します。

fun main(args: Array<String>) = runBlocking() {
    println("start")
    val text = async {
        "coroutine!"
    }.await()
    println(text)
    println("end")
}

Suspending Function

ところで、 join メソッドや await メソッドのような 、Coroutine を中断させることのできる関数を Suspending Function と言います。
suspend modifier を付けて関数を宣言することで、その関数が Coroutine を中断可能であることを示すことができます。

suspend fun doSomething() {
    delay(1000)
    println("something")
}

この例では doSomething 関数は 1 秒待ってから "something" と出力します。
delay も Suspending Function として定義されており、その Coroutine の処理を指定した時間だけ中断(停止)させることができます。

suspend と宣言された関数は Coroutine 内または他の Suspending Function 内からしか呼び出すことができません。
また Coroutine を開始するには最低 1 つの Suspending Function がなければなりません。
先程から launchrunBlocking ではラムダを使用して Coroutine を作成していましたが、実はこれらは無名の Suspending Lambda だったのです。

launch のスキーマは次のようになっています。

public fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

第3引数が匿名 Suspending Lambda になっています。

これらを踏まえた上で、次のようにプログラムを変更します。

fun main(args: Array<String>) {
    println("start")
    runBlocking {
        println("coroutine start")
        greet()
        println("coroutine end")
    }
    println("end")
}

suspend fun greet() {
    delay(1000)
    println("Hello, Coroutine!")
}

"start", "coroutine start" とすぐに表示され、 1 秒間経ってから "Hello, Coroutine!", "coroutine end", "end" と順に出力されます。

定義した greet() 関数が runBlocking で作成した Coroutine の処理を中断し、再開しているのがわかります。

まとめ

  • Coroutine は軽量な Thread と考えることができる
  • Coroutine を作成するコストは Thread とは比較できないほど軽微である
  • launch, runBlocking, async などを用いて Coroutine を開始できる
  • runBlocking では同期的に、 async では非同期的に Coroutine の戻り値を取得できる
  • Suspending Function とは Coroutine を中断可能な関数のことである
  • Coroutine を実行するには Suspending Function が必要である
  • launch, runBlocking, async などは引数に匿名 Suspending Funtion を取ることで Coroutine を開始している
  • Suspending Function は Coroutine か Suspending Function からしか実行できない
  • Coroutine 楽しい!

参考

405
296
1

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
405
296

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?