What is it, naokirin?

Kotlinのコルーチンについて今さらまとめておく

前回の記事で、コレクション関数をまとめました。

naokirin.hatenablog.com

今回はコルーチンについてまとめておきます。

コルーチンとは

コルーチンはある処理を中断・再開できるインスタンスを指します。

スレッドもこのような動作を行うことができますが、コルーチンのほうが一般的にリソース消費量が少ないとされています。またスレッド間でのデータのやり取りよりもコルーチンとのデータのやり取りのほうが簡単です。このような理由でスレッドではなく、コルーチンが用いられることがあります。

実際に簡単なコルーチンを実行してみます。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World")
    }
    println("Hello")
}

// output:
//   Hello
//   World

一般的な上から順に実行されていないことがわかります。

runBlocking でコルーチンのスコープを生成します。

launch で実際に新しいコルーチンを生成し、そのまま実行します。

async, await

launch を紹介しましたが、 async , await によるコルーチンの実行も可能です。

launch では、データの返却ができませんでしたが、 async , await ではコルーチンから単一のデータを返すことができます。

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val n = 2
    val plus_1 = async {
        delay(1000L)
        n + 1
    }
    val mult_3 = async {
        delay(1000L)
        n * 3
    }

    val t = measureTimeMillis {
        val amount = plus_1.await() + mult_3.await()
        println(amount)
    }
    println("Time: ${t}ms")
}

// output:
//   9
//   Time: 1008ms

asyncDeferred オブジェクトを返します。これは特定の型のインスタンス後で返すオブジェクトです。

Deferred に対して、 await を呼び出すことで最終的なデータを受け取ることができます。

Flow

Flow はこれまでと異なり、複数の値を返すことができます。返す際は、 emit 関数を利用します。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    simple().collect { value -> println(value) } 
}

// output:
//   I'm not blocked 1
//   1
//   I'm not blocked 2
//   2
//   I'm not blocked 3
//   3

Flowemit は、 Sequenseyield を知っている人であれば、その関係によく似ていることがわかると思います。

Channel

ChannelFlow と同じく複数の値をコルーチンとの間で受け渡すことができます。ただし、 Flowcollect といった終端になるメソッド呼び出しが行われるまで実行されませんが、 Channel は単にコルーチンから呼び出されるだけなので、コルーチン自体は自動的に動き続けます。

そのため、 Channel では受け取り側の準備に関わらず、データを送信することになります。受け取るまでの間、 Channelインスタンスでデータを受け持つ形になります。そのため、指定したバッファを超えてデータが溜まるとエラーとなります。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
        channel.close()
    }
    for (y in channel) println(y)
    println("Done!")
}

// output:
//   1
//   4
//   9
//   16
//   25
//   Done!

ちなみに、コルーチン側から送るだけでなく、コルーチンに向けて送ることも可能です。

サスペンド関数

コルーチンでは、コルーチンの処理を中断できる関数を呼ぶことができ、これをサスペンド関数と呼びます。これまでのサンプルコードでも delay というサスペンド関数を利用しています。

サスペンド関数は、通常の関数と違い、コルーチンのスコープ内か、サスペンド関数からしか呼び出すことができません。

サスペンド関数を定義して利用してみます。

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun delay_2_sec() {
    delay(2000L)
}

fun main() = runBlocking<Unit> {
    launch {
        println("call delay.")
        val time = measureTimeMillis {
            delay_2_sec()
        }
        println("finish delay. $time msec")
        
    }
    println("block ended.")
}

// output:
//   block ended.
//   call delay.
//   finish delay. 2004 msec

サスペンド関数は suspend キーワードを付けて関数を定義します。これにより複雑なコルーチンも、関数に処理をまとめる事ができます。

まとめ

今回は簡単なコルーチン周りの使用方法をまとめました。

キャンセル等の説明を省きましたが、コルーチンの真骨頂はキャンセルもできることにあるかと思います。 launchasync/awaitFlowChannel を使い分けていくことで、ノンブロッキングな処理を簡単に達成できるので活用していきたいですね。