検証環境
この記事の内容は、以下の環境で検証しました。
- Intellij IDEA ULTIMATE 2018.2
- Kotlin 1.3.0
- Gradle Projectで作成
- GradleファイルはKotlinで記述(KotlinでDSL)
やりたいこと
Kotlin 1.3のリリースと同時に、これまで実験的だったCoroutineの1.0.0がリリースされました。
早速、公式サイトで内容を確認してみると思いの外難しいです。そのため、少しづつ翻訳して、実験しながら理解していいくことにしました。現時点では、Coroutineが何者かも今はわかりません。
最終的には、コルーチンを完全に理解します。
※翻訳の際、意訳が交じることがあります、ご注意ください。
準備
Coroutineを利用するには、ライブラリを追加する必要があります。
渡しの場合、gradleもKotlinで記述しているので、下記のような記述になります。
dependencies {
implementation( "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0")
}
基本的に、下記の公式ように追記すれば問題ありません。
Coroutine-Using in your projects
intellijでの設定がわからない方は下記を参照するといいと思います。
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)
}
確かにエラーが発生しました。エラーの種類はコンパイルエラーのようです。
やはり、サスペンド関数っていうのが、キモのような気がしてきます。
公式サイトでは、このブロック最後の説明です。
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関数が特別なようです。
ここでいう thread は thread関数 で生成したスレッドのことを指していると思われます。
delay関数の実装部分をみてみると
public suspend fun delay(timeMillis: Long) {
〜省略〜
}
確認すると関数の宣言で suspend なるキーワードが使われています。
まとめ
ここまででわかったことは、以下になると思います。
- コルーチンはスレッドを生成して実行する処理であること
- Suspend(サスペンド)なるものがあること
- サスペンド関数はコルーチンかサスペンド関数からしか呼び出せないこと