そろそろやってみるべき?
まだexperimentalですが、そろそろ正式版になるようですし(Kotlin 1.3予定)、production readyらしいし、身につけておきたい技術になってきました。
こちらにコルーチンのチュートリアルがあるのを発見したのでやってみながら翻訳しました
https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html
async await自体はさまざまな言語にもありますし、そういう意味でも、ちゃんと理解しておくべきかなと思いました。
コルーチンはKotlin 1.1で非同期処理の新しい方法として導入されました。
見てみて思ったのですが、すごく基本的で初歩的なのですが、割と導入には分かりやすくて良いと思いました。
これで心置きなくk-kagurazakaさんの入門Kotlin coroutines に行けそう?な気がします。
プロジェクト作成
IntelliJでKotlinのプロジェクトを作ります。
ここからIntelliJをインストールしましょう。Communityで大丈夫です。簡単です。
https://www.jetbrains.com/idea/download/?fromIDE=#section=mac
プロジェクト作成時にKotlinで作るとコルーチンを有効にする方法が分からなかったので、Gradleで作りましょう。
apply plugin: 'kotlin'
kotlin {
experimental {
coroutines 'enable'
}
}
現時点での最新版は0.22.3みたいなのでそれを入れます。(ここでみれそう
https://github.com/Kotlin/kotlinx.coroutines/releases )
dependencies {
...
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.3"
}
repositories {
jcenter()
}
以下の文章はほぼ翻訳になります
最初のコルーチン
コルーチンは簡単なスレッドとして考えることが出来ます。スレッドのようにコルーチンは並列で走ることができ、それぞれが待って連携することができます。一番の違いはコルーチンはとてもパフォーマンス的に安価(cheap)でほとんどただで使うことが出来ます。1000個作ることだって出来てパフォーマンス的にはほとんど影響がありません。一方本当のスレッドでは開始して保持することは高価です。1000スレッドだと重大な問題になる事があります。
launch {}
を使って、コルーチンを始めてみよう
launch {
...
}
デフォルトではコルーチンはスレッドプールを利用して動作します。コルーチンのプログラムでもスレッドはまだ存在しますが1つのスレッドがたくさんのコルーチンを実行することができます。そのためたくさんのスレッドが必要ありません。
launch
を使ったプログラムを見てみましょう。
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>) {
println("Start")
launch {
delay(1000)
println("Hello")
}
Thread.sleep(2000)
println("Stop")
}
1秒待ってから"Hello"が表示されます。
今回、delay()
を使いました。それはThread.sleep()
のようです。しかしもっと良いです。delay()
はスレッドをブロックしません。ただコルーチンだけを一時停止します。スレッドはコルーチンが待っている間はプールに戻っていて、待ち終わったらコルーチンはプールの中の自由なスレッドによって復帰します。
メインスレッド(main()関数を実行している)はコルーチンが終わるのを待つ必要がある、そうでなければHelloが出力されるまでにプログラムが終了してします。
Exercise: sleep()を消して結果を見よう
→ StartとStopだけ出力される。
もしdelay()
をmain()関数の中で直接呼び出すと、エラーになります。
Suspend functions are only allowed to be called from a coroutine or another suspend function
なぜならコルーチンの中でないためです。コルーチンを開始して、コルーチンが終了するのを待つrunBlocking {}
でラップすることによってdelayを使うことができます。
runBlocking {
delay(2000)
}
最初にStartをprintしてlaunch {}
によってコルーチンを開始、そしてもう一つrunBlocking {}
が行われ、それが終わるまでブロックされ、そしてStop
が出力されます。それまでの間に最初のコルーチンが完了し、Hello
が出力されます。これは話しているようにスレッドのようです。
たくさんのコルーチンを動かしてみよう
コルーチンはスレッドより安価なのを確かめてみましょう100万のコルーチンを動かしたらどうなるでしょうか?まず最初に100万のスレッドを動かしてみましょう。
val c = AtomicInteger()
for (i in 1..1_000_000)
thread(start = true) {
c.addAndGet(i)
}
println(c.get())
これは1'000'000のスレッドが1つのカウンタを上げていっています。このプログラムが終わる前に私の忍耐が切れました。(おそらく1分以上)
これをコルーチンでやってみましょう。
val c = AtomicInteger()
for (i in 1..1_000_000)
launch {
c.addAndGet(i)
}
println(c.get())
私の環境では1秒以内にこれは終わるが気まぐれな数字が返ってきます。なぜならいくつかのコルーチンはmain()関数のprintが行われる前に終わらなかったからです。これを修正してみましょう。
スレッドと同様の同期手段を利用できます。(この場合CountDownLatchが思い浮かぶ。)しかしもっと安全でもっときれいな方法でやってみよう。
Async: コルーチンから値を返す
コルーチンを開始するもう一つの方法がasync {}
です。async {}
はlaunch {}
のようですがDeferred<T>
型を返却します。Deferred<T>
はawait()
関数を持っていて、コルーチンの結果を返します。Deferred<T>
は基本的なfutureです(JDKのfutureもサポートされていますが、現在はDeferred
に限定しています。)
Deferredオブジェクトを保持して、もう一度100万のコルーチンを作ってみましょう!これからはAtomicCounterは必要なく、コルーチンでただ追加される値を返すだけで良いです。
runBlocking {
val sum = deferred.sumBy { it.await() }
println("Sum: $sum")
}
それらはすでに始まっているので私たちはその結果を集める必要があります。
val sum = deferred.sumBy { it.await() }
私たちはただそれぞれのコルーチンを取り、結果をawaitして待って、標準ライブラリのsumBy()
で合計しているだけです。ただコンパイラは怒ります。
Suspend functions are only allowed to be called from a coroutine or another suspend function
await()
はコルーチンの外で呼ぶことができません。なぜなら計算結果が出るまで一時停止する必要があり、コルーチンだけがブロックせずに一時停止することができるためです。なので、コルーチンの中に書いてみましょう。
runBlocking {
val sum = deferred.sumBy { it.await() }
println("Sum: $sum")
}
この結果1784293664
が出力されます。なぜなら全てのコルーチンが終わったためです。
そしてこれが並列で動いていることを確認してみましょう。もし1秒のdelay()
をそれぞれのasync
に追加したとして、1'000'000秒(11.5日)かからないです。
val deferred = (1..1_000_000).map { n ->
async {
delay(1000)
n
}
}
これはだいたい10秒かかります。つまりコルーチンは並列で動いているのです。
コルーチンを中断する
さて、1秒待ってから値を返すというのをメソッドに分割したいとします。
fun workload(n: Int): Int {
delay(1000)
return n
}
優しいエラーが表示されます。
Suspend functions are only allowed to be called from a coroutine or another suspend function
(suspendの関数だけがコルーチンや他のsuspendの関数から呼び出すことができます。)
この意味をちょっと深掘りしてみましょう。コルーチンの一番のメリットはスレッドをブロックしないことです。コンパイラはこれを実現するために特別なコードを生成します。そのため、私たちはコード内で明白に一時停止することを印としてつける必要がある。私たちはsuspend修飾子をそれのために使います。
suspend fun workload(n: Int): Int {
delay(1000)
return n
}
これでコンパイラはそれが一時停止することを知り、私たちはworkload()
をコルーチンから呼び出すことが出来るようになりました。そして以下のように呼び出します。
async {
workload(n)
}
workload()
関数はコルーチン(か他のsuspend関数)から呼び出すことができるが、コルーチンの外からは呼び出すことができません。delay()
とawait()
はそれ自体がsuspend
として宣言されています。そしてそれがrunBlocking {}
, launch {}
またはasync {}
の中に書かなければいけなかった理由です。