KotlinのCoroutineを試しに使ってみる
KotlinにはCoroutineと呼ばれるものがあるので、今回はこちらを使ってみます。
ドキュメントはこちらの公式ガイドを参考にしました。
Coroutine とは?
一通りKotlinで試したあとに分かったのですが、Kotlin固有の言葉ではないようです。wikiによるとCoroutineはSubroutineとは異なり中断・再開できる処理のまとまりということらしいです。
KotlinのCoroutineによって、Kotlinのコードを中断・再開可能な単位に構成することができるようになります。それだけではなく、中断したCoroutineが再開するスレッドは異なっても良いためCPUのコアを効率的に使用でき、スループットの向上が期待できます。IO待ちが発生する状況や大量の計算を非同期処理したい場合に役立ちそうですね。
少しの期間触ってみて感じたことは、次の3要素がKotlin Coroutineにおける主要登場人物であることです。
- Coroutine Builder
- Suspend Function
- Coroutine Scope
Hello, Coroutine!
例えば次のように書きます。
package booookstore.playground
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class HelloCoroutineTest {
@Test
fun sayHello() {
var message = ""
runBlocking { //#1
launch { //#2
delay(100L) //#3
message += "World. " // <2>
}
message += "Hello, " // <1>
}
message += "from coroutine." // <3>
assertEquals("Hello, World. from coroutine.", message)
}
}
#1 runBlocking
はCoroutineBuilderというものです。Builderと呼ばれるとおりCoroutineを作成します。 runBlock
はNone-Coroutineの世界からCoroutineの世界へのエントリポイントです。{ }
の内部ではCoroutineを自由に生成することができます。Blockingと呼ばれるとおり、Coroutineの実行完了まで現在のスレッドをブロックします。言い換えるとCoroutineの実行が完了するまでスレッドは次のコードを実行しません。
#2 launch
もCoroutineBuilderの一つです。そう、CoroutineBuilderは複数登場しそれぞれ役割が少々違います。 launch
はCoroutineを新しく生成しますが現在のスレッドをブロックしません。suspend関数によって実行が中断されると現在のスレッドを開放します。開放されたスレッドは他の仕事をすることができます。
#3 delay
はsuspend関数の一つです。suspend関数は現在のCoroutineを中断する関数です。ここでは0.1秒Coroutineを中断します。0.1秒後(正確には0.1秒後Coroutineがいずれかのスレッドに割り当てられてから)再開します。
上記のプログラムは <1> <2> <3> の順序でmessage変数にアクセスし、最後のアサーションに成功します。
大雑把なイメージにすると以下となります。
CoroutineBuilder
CoroutineBuilderはCoroutineを新しく作成します。runBlocking、launch 、 async はそれぞれ代表的なCoroutineBuilderです。
CoroutineBuilder | 戻り値 | 計算結果 |
---|---|---|
launch | Job | 計算結果を返さない |
async | Deffered | 計算結果を返す |
runBlocking | 任意 | 計算結果を返す |
CoroutineBuilderがCoroutineを作成するイメージはこんな感じになります。
launch
は Job
を返します。 join
を使うことでCoroutineの完了を待機できます。
package booookstore.playground
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class JobTest {
@Test
fun job() = runBlocking {
var message = ""
val job = launch {
delay(100L)
message += "hello"
}
job.join()
assertEquals("hello", message)
}
}
Job
はCoroutineの計算結果を持っていません。つまり、 launch
は計算結果を返す必要のないCoroutineを生成します。
一方で async
は Deffered
を返します。Deffered
もJob
と同様にjoin
により完了を待機できます。Coroutineの完了後に getCompleted
を呼び出せば計算結果を受け取れます。
package booookstore.playground
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class AsyncTest {
@Test
fun async() = runBlocking {
val deferred = async {
delay(100L)
"hello"
}
deferred.join()
assertEquals("hello", deferred.getCompleted())
}
}
Job
や Deffered
が複数ある場合はリストにしてすべての完了を待機することもできます。
次は Job
が複数ある場合のケースです。
package booookstore.playground
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class JobTest {
@Test
fun job() = runBlocking {
var message = ""
val jobA = launch {
delay(100L)
message += "hello"
}
val jobB = launch {
delay(200L)
message += ",world"
}
listOf(jobA, jobB).joinAll()
assertEquals("hello,world", message)
}
}
次は Deffered
が複数ある場合のケースです。
package booookstore.playground
import kotlinx.coroutines.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class AsyncTest {
@Test
fun async() = runBlocking {
val deferredA = async {
delay(100L)
"hello"
}
val deferredB = async {
delay(100L)
"world"
}
listOf(deferredA, deferredB).joinAll()
assertEquals("hello", deferredA.getCompleted())
assertEquals("world", deferredB.getCompleted())
}
}
サンプルコードでは伝わりづらいですがCoroutineを任意の数作成することはよくあることだと思うので上記のやり方を覚えておくと良さそうです。
Suspend function
後述するCoroutineScope内でSuspend functionを呼び出すことができます。さらに、他のSuspend functionからもSuspend functionを呼び出すことができます(以下のサンプルコードを参照)。Suspend functionは中断可能な関数で、例えばネットワーク通信やIOの処理などで処理を中断します。例えば上記の例だと delay
はSuspend functionです。
Suspend functionによって中断されるとCoroutineは現在のスレッドを開放します。このスレッドの開放がパフォーマンスにおいて重要な点になります。Coroutine無しでIO処理などを行うとスレッドをブロックするためその間スレッドは何も仕事をせず待機する事になります。一方、Coroutineの場合スレッドを開放するので、開放されたスレッドは他の処理に割り当てることができます。
Suspend functionを使用するコードを関数に切り出す場合、その関数もまたSuspend functionと名乗る必要が有り、先頭に suspend
と宣言します。
次のコードでは2つのSuspend functionを定義しています。Suspend functionからSuspend functionを呼び出すことができるため、 message1
は message2
を呼び出しています。この例では合計で0.2秒関数が中断されます。 message1
は単に message2
を呼び出しているだけなのでdelayで指定した時間の合計が中断される時間の合計と一致するというわけです。
package booookstore.playground
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class SuspendFunctionTest {
@Test
fun suspendFunctionTest() = runBlocking {
val message = message1()
assertEquals("hello world", message)
}
private suspend fun message1(): String {
delay(100L)
val message = message2()
return "hello$message"
}
private suspend fun message2(): String {
delay(100L)
return " world"
}
}
一方で下記のようにすることで0.1秒の中断で済みます。2回のSuspend functionの呼び出しをそれぞれ別々のCoroutineで行っているので一つのスレッドが片方が中断されている間にもう片方の処理を継続できるからです。
@Test
fun suspendFunction2Test() = runBlocking {
val deferredA = async { message2() }
val deferredB = async { message2() }
assertEquals(" world world", deferredA.await() + deferredB.await())
}
CoroutineScope
CoroutineScopeは名前から想像できる通り、Coroutineが存在できるスコープのことになります。Coroutineが存在するならば、必ずCoroutineScopeも同時に存在することになります。CoroutineScope自体は概念ではなく、インターフェイスとして存在します。
launch
や async
といった関数は CoroutineScope
に対する拡張関数として定義されています。なので、Coroutineを作成する launch
や async
はCoroutineScope内にて呼び出すことが強制されます。また、これら2つの関数は CoroutineScope
をレシーバーとして持つ関数を引数で受け取ります。2つの関数はCoroutineBuilderなのでCoroutineを作成し、その上で何をするのかを引数で受け取ることができるというわけですね。
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
ここまでの話だと「Coroutineには一つのCoroutineScopeしか存在しないので意識する必要がないのでは?」と思うかもしれません。しかし、 coroutineScope
関数を使うことで一つのCoroutineの中に複数のCoroutineScopeを持つことができます。CoroutineScopeを作成する関数をScopeBuilderと言います。
例えば、次の例では2つのCoroutineScopeが1つのCoroutineの中に存在しています。
package booookstore.playground
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@Suppress("NonAsciiCharacters")
class CoroutineScopeTest {
@Test
fun coroutineScope() = runBlocking {
val message = secondCoroutineScope()
assertEquals("hello", message)
}
private suspend fun secondCoroutineScope(): String = coroutineScope {
async {
delay(100L)
"hello"
}.await()
}
}
図にすると以下のようなイメージになります。
runBlocking
もScopeBuilderです。runBlocking
coroutineScope
はどちらもCoroutineの実行が終わるまで呼び出し元の処理を止めますが、 runBlocking
は元なるスレッドを開放しません。一方、 coroutineScope
はスレッドを開放します。このような違いから runBlocking
はCoroutineの世界へのエントリポイントとしての役割を持っています。main関数やテストの中で呼び出す想定をしています。Coroutineの世界で runBlocking
を呼び出してしまうと元となるスレッドを開放してくれないため、Coroutineであるべきメリットを握りつぶしてしまうことになります。Coroutineの世界では coroutienScope
を使うべきです。
代表的な関数の違い
ここまで登場した関数の違いを整理しました。
関数 | 元となるスレッドを開放 | 呼び出し元はCoroutine完了を | 新しくCoroutineを作成 |
---|---|---|---|
launch | する | 待たない | する |
async | する | 待たない | する |
runBlocking | しない | 待つ | する |
coroutineScope | する | 待つ | しない |
Discussion