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のlaunch
でdelay()
してる間に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] : : :
サンプルコードは下記にあげました。
おわり。