Kotlin
coroutine

Kotlin 1.3のCoroutineの使い方①(はじめてのCoroutine)


検証環境

この記事の内容は、以下の環境で検証しました。


  • Intellij IDEA ULTIMATE 2018.2

  • Kotlin 1.3.0

  • Gradle Projectで作成

  • GradleファイルはKotlinで記述(KotlinでDSL)


やりたいこと

Kotlin 1.3のリリースと同時に、これまで実験的だったCoroutineの1.0.0がリリースされました。

早速、公式サイトで内容を確認してみると思いの外難しいです。そのため、少しづつ翻訳して、実験しながら理解していいくことにしました。現時点では、Coroutineが何者かも今はわかりません。

最終的には、コルーチンを完全に理解します。

※翻訳の際、意訳が交じることがあります、ご注意ください。

公式:Coroutineのページ


準備

Coroutineを利用するには、ライブラリを追加する必要があります。

渡しの場合、gradleもKotlinで記述しているので、下記のような記述になります。

dependencies {

implementation( "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0")
}

基本的に、下記の公式ように追記すれば問題ありません。

Coroutine-Using in your projects

intellijでの設定がわからない方は下記を参照するといいと思います。

intellijでkotlinプロジェクトを作る方法


Your first coroutine

公式サイトでは、はじめてのコルーチンといった感じでサンプルコードが載っています。

Your first coroutine


Run the following code:


「まずは、下記のコードを実行せよ」と言われたので、実際に動かしてみました。

import kotlinx.coroutines.*

fun main() {
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main thread continues here immediately
Thread.sleep(2000L)
}

実行結果:

Hello,

World!

公式サイトと同様に表示されたので一安心です。

しかし、釈然としません。 初めてみるクラスや関数が多いです。

1つずつ調べてみました。

クラスや関数
説明

GlobalScope

GlobalScopeはトップレベルのコルーチンを起動できます。しかし、通常はアプリごとのスコープCoroutineScope、を利用すべきです。

launch関数
新しいコルーチンを起動します。その際、起動したスレッドを止めることはしません。(同期はとらない)なので、今回のサンプルコードだとmain関数の処理を止めることなく、処理を実行します。戻り値としてJob型の値を返します。さらにJobを利用するとキャンセルも可能です。

delay関数
main関数のスレッドを止めずに、コルーチンを一時停止させます。引数にミリ秒を指定します。

どうやらコルーチンはmain関数のスレッドとは別物として実行するみたいです。

スレッドが本当に違うのか、下記のコードを実行し、確認してみました。

package step01

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.LocalTime

fun main(args: Array<String>) {

println("main関数を実行しているスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

GlobalScope.launch {
println("GlobalScope.launch拡張関数直下のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

delay(1000L)
println("World!")
}

println("Hello直前のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

println("Hello,")
Thread.sleep(2000L)
}

所々に、スレッドのIDと名前を表示する処理を追記してみました。

実行してみると以下のような結果になりました。

main関数を実行しているスレッドはid:1、name:main¥n で時間は17:03:49.722048

GlobalScope.launch拡張関数直下のスレッドはid:12、name:DefaultDispatcher-worker-1¥n で時間は17:03:49.836755
Hello直前のスレッドはid:1、name:main¥n で時間は17:03:49.862578
Hello,
World!

main関数のスレッドはIDが 1、コルーチンは 12 であるため、別のスレッドであることがわかります。

では、引き続き公式サイトを読み進めていきます。

次はこのように記述しています。


Essentially, coroutines are light-weight threads. They are launched with launch coroutine builder in a context of some CoroutineScope.

Here we are launching a new coroutine in the GlobalScope, meaning that the lifetime of the new coroutine is limited only by the lifetime of the whole application.


意訳込みですが、こんなこと行っています。


コルーチンは軽量なスレッドです。CoroutineScopeのcontextにあるコルーチンビルダーを起動し、そして、コルーチンを最終的に起動します。

今回は、GlobalScopeで起動しているため、このコルーチンの寿命はアプリケーションと同じです。


なるほど、CoroutineScopeを利用するべきなんですね。

続けて読み進めていきます。


You can achieve the same result replacing GlobalScope.launch { ... } with thread { ... } and delay(...) with Thread.sleep(...). Try it.


意訳込みですが訳すと・・・


GlobalScope.launch { ... } はthread { ... }に、delay(...) は Thread.sleep(...)に置き換えられるんで、試してみよう!同じ結果になるはずだよ。


公式サイトに載っている通り、置き換えてみました。

置き換え後のコードは以下のようになります。

fun main(args: Array<String>) {

println("main関数を実行しているスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

thread {
println("GlobalScope.launch拡張関数直下のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

Thread.sleep(1000L)
println("World!")
}

println("Hello直前のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

println("Hello,")
Thread.sleep(2000L)
}

特にコンパイルエラーもないので、実行してみます。

実行結果は以下のようになります。

main関数を実行しているスレッドはid:1、name:main¥n で時間は17:28:47.395996

GlobalScope.launch拡張関数直下のスレッドはid:12、name:Thread-0¥n で時間は17:28:47.399776
Hello直前のスレッドはid:1、name:main¥n で時間は17:28:47.399904
Hello,
World!

確かに、同じ結果になりました。

置き換えも出来たところで、読み進めていきます。


If you start by replacing GlobalScope.launch by thread, the compiler produces the following error:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function


意訳込みですが、


もし、GlobalScope.launchだけをthreadに置き換えると、コンパイルエラー発生するよ。

エラー:Suspend(サスペンド)関数はコルーチンかその他のサスペンド関数からのみ呼び出せるよ


実際にコードを記述してみます。

fun main(args: Array<String>) {

println("main関数を実行しているスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

thread {
println("GlobalScope.launch拡張関数直下のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

delay(1000L)
println("World!")
}

println("Hello直前のスレッドはid:${Thread.currentThread().id}、name:${Thread.currentThread().name}" +
"¥n で時間は${LocalTime.now()}")

println("Hello,")
Thread.sleep(2000L)
}

確かにエラーが発生しました。エラーの種類はコンパイルエラーのようです。

スクリーンショット 2018-11-09 9.22.20.png

やはり、サスペンド関数っていうのが、キモのような気がしてきます。

公式サイトでは、このブロック最後の説明です。


That is because delay is a special suspending function that does not block a thread, but suspends coroutine and it can be only used from a coroutine.


意訳込みですが


この原因(delay関数の呼び出しがコンパイルエラーになる原因)は、delay関数が特別なサスペンド関数からです。ここでいう特別とは、threadをブロックしません。しかし、コルーチンかサスペンド関数からしか呼び出せないよ。


やっぱりdelay関数が特別なようです。

ここでいう threadthread関数 で生成したスレッドのことを指していると思われます。

delay関数の実装部分をみてみると

public suspend fun delay(timeMillis: Long) {

〜省略〜
}

確認すると関数の宣言で suspend なるキーワードが使われています。


まとめ

ここまででわかったことは、以下になると思います。


  • コルーチンはスレッドを生成して実行する処理であること

  • Suspend(サスペンド)なるものがあること

  • サスペンド関数はコルーチンかサスペンド関数からしか呼び出せないこと