KotlinのCoroutineを試す (basic)

kotlinのcoroutineを試してみたメモです。

kotlin公式のcoroutineチュートリアルのbasicの写経とメモです。
公式を見たほうが最新で正確な情報が得られます。
https://kotlinlang.org/docs/coroutines-guide.html

下記バージョンで試してみます。

  • kotlin 1.4.31
  • kotlinx-coroutines-core:1.4.3

Dependency

gradleを使って試してみます。

build.gradle

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.4.31'
}

group 'com.example.coroutine.kotlin'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
}

compileKotlin {
    kotlinOptions.jvmTarget = "11"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "11"
}

Coroutine Basics

first coroutine

coroutineのHello worldです。

MyFirstCoroutin.kt

fun main() {
    GlobalScope.launch {
        delay(1_000L)
        println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
    }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
    Thread.sleep(2_000L)
}

CoroutineScope.launchでcoroutineが生成され実行されます。
ここではGlobalScopeで実行しています。
delay()でnon-blockingで1秒待機します。
最後のThread.sleep()はmain threadが終わらないように待機しています。

実行

[2020-12-23T19:34:29.212384600Z] Hello, [main]
[2020-12-23T19:34:30.232232100Z] World! [DefaultDispatcher-worker-1]

Hello,がmain threadで、World!がworkerで実行されていることが分かります。

Bridging blocking and non-blocking worlds

先程のbasicの例はdelay()Thread.sleep()が同じコードに混在していたので、
どちらがblockingか、non-blockingか分からなくなります。
runBlocking()を使用して、blockingであることを明示的にしてみます。

BridgingBlockingAndNonBlockingWorld.kt

fun main() {
    GlobalScope.launch {
        delay(1_000L)
        println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
    }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
    runBlocking {
        println("[${Instant.now()}] ...delay..., [${Thread.currentThread().name}]")
        delay(2_000L)
    }
}

実行

[2021-03-10T18:46:46.179114200Z] Hello, [main]
[2021-03-10T18:46:46.195117800Z] ...delay..., [main]
[2021-03-10T18:46:47.197201100Z] World! [DefaultDispatcher-worker-1]

Thread.sleep()の代わりにdelay()を使用してますが、
同様にHello,がmain threadで、World!がworkerで実行されていることが分かります。

runBlocking()でmain関数をwrapするほうが一般的な書き方です。

UsingRunBlocking.kt

fun main() = runBlocking<Unit> { // <Unit>は省略可能
    GlobalScope.launch {
        delay(1_000L)
        println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
    }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
    delay(2_000L)
}

実行

[2021-03-10T18:56:56.980930400Z] Hello, [main]
[2021-03-10T18:56:57.991155700Z] World! [DefaultDispatcher-worker-1]

Waiting for a job

delay()で時間を指定して別のcoroutineの終了を待つのはよい方法ではないので、
launchの戻り値のJobを保持して、join()で完了を待つように変更します。

WaitingForAJob.kt

fun main() = runBlocking {
    val job = GlobalScope.launch {
        delay(1_000L)
        println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
    }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
    job.join()
}

実行

[2021-03-10T18:57:53.673737800Z] Hello, [main]
[2021-03-10T18:57:54.683456200Z] World! [DefaultDispatcher-worker-1]

Structured concurrency

先程の例ではGlobalScopeでcoroutineを起動しましたが、GlobalScopeを使用した場合トップレベルでcoroutineが起動されます。
この場合、起動したcoroutineの参照を保持せずに起動することもできるので、
起動したcoroutineがハングしたり、起動しすぎてメモリ不足になった場合のために
coroutineの参照を保持してjoin()する必要があります。

よい方法としては、GrobalScopeではなく、実行している処理の特定のscopeでcoroutineを起動します。
runBlocking()で作成されたCoroutineScopeのcoroutineの中で、launchを実行します。
runBlocking()で作成されたcoroutineは、そのscope内で起動されたすべてのcoroutineが完了してから完了するので、
明示的にjoin()する必要がなくなります。

StructuredConcurrency.kt

fun main() = runBlocking {
    launch {
        delay(1_000)
        println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
    }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
}

実行

[2021-03-10T19:07:07.783903300Z] Hello, [main]
[2021-03-10T19:07:08.814903900Z] World! [main]

Scope builder

coroutineScope()を使用して独自のscopeを作成することができます。
そのscope内で起動されたすべてのcoroutineが完了してから完了します。

ScopeBuilder.kt

fun main() = runBlocking {
    launch {
        delay(200L)
        println("[${Instant.now()}] Task from runBlocking [${Thread.currentThread().name}]") // (1)
    }

    coroutineScope {
        launch {
            delay(500L)
            println("[${Instant.now()}] Task from nested launch [${Thread.currentThread().name}]") // (2)
        }

        delay(100L)
        println("[${Instant.now()}] Task from coroutine scope [${Thread.currentThread().name}]") // (3)
    }

    println("[${Instant.now()}] Coroutine scope is over [${Thread.currentThread().name}]") // (4)
}

runBlocking()は通常の関数なのでthreadをブロックして待機しますが、
coroutineScope()はsuspend関数なのでthreadをブロックせずに開放します。

runBlocking()coroutineScope()の定義を見てみると下記のようになっています。

fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T (source)
suspend fun <R> coroutineScope(
    block: suspend CoroutineScope.() -> R
): R (source)

実行

[2021-03-11T18:14:24.465141700Z] Task from coroutine scope [main]
[2021-03-11T18:14:24.570142400Z] Task from runBlocking [main]
[2021-03-11T18:14:24.869564400Z] Task from nested launch [main]
[2021-03-11T18:14:24.869564400Z] Coroutine scope is over [main]

実行すると3, 1, 2, 4の順で表示されます。
1のlaunchdelay()してる間に2のcroutine scopeでlaunchが起動され、2がdelay()してる間に3が表示されます。

Extract function refactoring

launch{}の中身を関数として切り出してみます。
切り出した関数はsuspendを付けます。
suspend関数はcoroutine内または他のsuspend関数からのみ呼び出すことが出来ます。

ExtractFunction.kt

fun main() = runBlocking {
    launch { doWorld() }
    println("[${Instant.now()}] Hello, [${Thread.currentThread().name}]")
}

suspend fun doWorld() {
    delay(1_000L)
    println("[${Instant.now()}] World! [${Thread.currentThread().name}]")
}

実行

[2021-03-11T18:53:52.673008300Z] Hello, [main]
[2021-03-11T18:53:53.701117200Z] World! [main]

Coroutines ARE light-weight

coroutineは非常に軽量です。
10万のcoroutineを起動し、5秒後に.をprintします。
これをthreadで実行しようとするとmemory不足になるでしょう。

CoroutinesAreLightWeight.kt

fun main() = runBlocking {
    repeat(100_000) {
        launch {
            delay(5_000L)
            print(".")
        }
    }
}

実行

........................()

Global coroutines are like daemon threads

GlobalScopeで実行されたcoroutineは途中で終了する場合があるため、daemon threadのようなものだ、
と言っているようです。

GlobalCoroutinesAreLikeDaemonThreads.kt

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1_000) { i ->
            println("[${Instant.now()}] I'm sleeping $i ... [${Thread.currentThread().name}]")
            delay(500L)
        }
    }
    delay(1_300L)
}

実行

[2021-03-11T19:03:55.213153300Z] I'm sleeping 0 ... [DefaultDispatcher-worker-1]
[2021-03-11T19:03:55.728794400Z] I'm sleeping 1 ... [DefaultDispatcher-worker-1]
[2021-03-11T19:03:56.230330800Z] I'm sleeping 2 ... [DefaultDispatcher-worker-1]

これは前のScope builderの章で説明されてましたが、
CoroutineScopeのscopeで実行すると処理が完了してから終了します。

fun main() = runBlocking {
    launch {
        repeat(1_000) { i ->
            println("[${Instant.now()}] I'm sleeping $i ... [${Thread.currentThread().name}]")
            delay(500L)
        }
    }
    delay(1_300L)
}

実行

[2021-03-11T19:10:29.090427400Z] I'm sleeping 0 ... [main]
[2021-03-11T19:10:29.604427800Z] I'm sleeping 1 ... [main]
[2021-03-11T19:10:30.106469500Z] I'm sleeping 2 ... [main]
[2021-03-11T19:10:30.607722700Z] I'm sleeping 3 ... [main]
[2021-03-11T19:10:31.109619300Z] I'm sleeping 4 ... [main]
[2021-03-11T19:10:31.611218400Z] I'm sleeping 5 ... [main]
[2021-03-11T19:10:32.112990300Z] I'm sleeping 6 ... [main]



 
サンプルコードは下記にあげました。

github.com

おわり。