コトリンコルーチン入門
1概要
この記事では、Kotlin言語からのコルーチンを調べます。簡単に言えば、
コルーチンは非常に流暢な方法で非同期プログラムを作成することを可能にします
、そしてそれらは
https://en.wikipedia.org/wiki/Continuation-passing
style[継続渡しスタイル]__プログラミングの概念に基づいています。
Kotlin言語は基本的な構成要素を与えてくれますが、
kotlinx-coroutines-core
ライブラリを使えばもっと役に立つコルーチンにアクセスすることができます。 Kotlin言語の基本的な構成要素を理解したら、このライブラリを調べます。
2
BuildSequence
を使用してコルーチンを作成する
buildSequence
関数を使用して最初のコルーチンを作成しましょう。
そして、この関数を使ってフィボナッチ数列ジェネレータを実装しましょう。
val fibonacciSeq = buildSequence {
var a = 0
var b = 1
yield(1)
while (true) {
yield(a + b)
val tmp = a + b
a = b
b = tmp
}
}
yield
関数のシグネチャは次のとおりです。
public abstract suspend fun yield(value: T)
suspend
キーワードは、この機能がブロックされる可能性があることを意味します。そのような関数は
buildSequence
コルーチンを中断することができます。
-
一時停止関数は標準のKotlin関数として作成できますが、コルーチン内からしか呼び出せないことに注意する必要があります。
__buildSequence内で呼び出しを中断した場合、その呼び出しはステートマシンの専用状態に変換されます。コルーチンは、他の関数と同様に変数に渡して代入することができます。
fibonacciSeq
コルーチンには、2つの中断点があります。まず、
yield(1)
を呼び出すとき、次に__yield(a b)を呼び出すときです。
その
yield
関数が何らかのブロッキング呼び出しになった場合、現在のスレッドはそれをブロックしません。他のコードを実行することができるでしょう。
中断された関数が実行を終了すると、スレッドは
fibonacciSeq
コルーチンの実行を再開できます。
フィボナッチ数列からいくつかの要素を取り出すことで、コードをテストできます。
val res = fibonacciSeq
.take(5)
.toList()
assertEquals(res, listOf(1, 1, 2, 3, 5))
3
kotlinx-coroutines
へのMaven依存関係の追加
基本的なコルーチンの上に構築された便利な構造を持っている
kotlinx-coroutines
ライブラリを見てみましょう。
依存関係を
kotlinx-coroutines-core
ライブラリに追加しましょう。
jcenter
リポジトリも追加する必要があることに注意してください。
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>0.16</version>
</dependency>
<repositories>
<repository>
<id>central</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
4
__launch()C
__oroutine
を使用した非同期プログラミング
kotlinx-coroutines
ライブラリには、非同期プログラムを作成できるようにする便利な構成要素が多数追加されています。入力リストに
String
を追加するという高価な計算関数があるとしましょう。
suspend fun expensiveComputation(res: MutableList<String>) {
delay(1000L)
res.add("word!")
}
そのサスペンド関数をノンブロッキングな方法で実行する
launch
コルーチンを使うことができます – スレッドプールを引数として渡す必要があります。
launch
関数は、結果を待つために
join()
メソッドを呼び出すことができる
Job
インスタンスを返しています。
@Test
fun givenAsyncCoroutine__whenStartIt__thenShouldExecuteItInTheAsyncWay() {
//given
val res = mutableListOf<String>()
//when
runBlocking<Unit> {
val promise = launch(CommonPool) {
expensiveComputation(res)
}
res.add("Hello,")
promise.join()
}
//then
assertEquals(res, listOf("Hello,", "word!"))
}
コードをテストできるように、すべてのロジックを
runBlocking
コルーチンに渡します。これはブロッキング呼び出しです。したがって、
assertEquals()
は、
runBlocking()
メソッド内のコードの後で同期的に実行できます。
この例では、
launch()
メソッドが最初にトリガーされますが、これは遅延計算です。メインスレッドは、
“Hello、” String
を結果リストに追加することによって進みます。
expensiveComputation()
関数で導入された1秒の遅延の後、
“ word!” String
が結果に追加されます。
5コルーチンは非常に軽量です
10万の操作を非同期的に実行したい状況を想像してみましょう。そのような多数のスレッドを生成すると非常にコストがかかり、__OutOfMemoryException.Oが発生する可能性があります。
幸いなことに、コルーチンを使用するとき、これは事実ではありません。必要なだけブロッキング操作を実行できます。内部的には、これらの操作は、過剰なスレッドを作成することなく、固定数のスレッドによって処理されます。
@Test
fun givenHugeAmountOfCoroutines__whenStartIt__thenShouldExecuteItWithoutOutOfMemory() {
runBlocking<Unit> {
//given
val counter = AtomicInteger(0)
val numberOfCoroutines = 100__000
//when
val jobs = List(numberOfCoroutines) {
launch(CommonPool) {
delay(1000L)
counter.incrementAndGet()
}
}
jobs.forEach { it.join() }
//then
assertEquals(counter.get(), numberOfCoroutines)
}
}
私たちは10万コルーチンを実行していて、実行ごとにかなりの遅延があることに注意してください。それにもかかわらず、
CommonPool.
のスレッドを使用してそれらの操作が非同期的に実行されるので、あまりにも多くのスレッドを作成する必要はありません。
6. キャンセルとタイムアウト
-
長時間実行される非同期計算をトリガーした後で、結果に関心がなくなったためにキャンセルしたい場合があります。**
launch()
コルーチンで非同期アクションを開始するときに、
isActive
フラグを調べることができます。メインスレッドが
Jobのインスタンスで
cancel()__メソッドを呼び出すと、このフラグはfalseに設定されます。
@Test
fun givenCancellableJob__whenRequestForCancel__thenShouldQuit() {
runBlocking<Unit> {
//given
val job = launch(CommonPool) {
while (isActive) {
println("is working")
}
}
delay(1300L)
//when
job.cancel()
//then cancel successfully
}
}
これは非常に洗練された
キャンセルメカニズムを使う
簡単な方法です。
非同期アクションでは、
isActive
フラグが
false
と等しいかどうかを確認して処理をキャンセルするだけです。
処理を要求していて、その計算にどのくらいの時間がかかるかわからない場合は、そのようなアクションにタイムアウトを設定することをお勧めします。指定されたタイムアウト時間内に処理が終了しない場合は、例外が発生します。適切に対処することができます。
たとえば、アクションを再試行できます。
@Test(expected = CancellationException::class)
fun givenAsyncAction__whenDeclareTimeout__thenShouldFinishWhenTimedOut() {
runBlocking<Unit> {
withTimeout(1300L) {
repeat(1000) { i ->
println("Some expensive computation $i ...")
delay(500L)
}
}
}
}
タイムアウトを定義しないと、計算がハングアップするため、スレッドが永久にブロックされる可能性があります。タイムアウトが定義されていないと、コードでそのケースを処理できません。
7. 非同期アクションを同時に実行する
2つの非同期アクションを同時に開始し、その後それらの結果を待つ必要があるとしましょう。処理に1秒かかり、その処理を2回実行する必要がある場合、同期ブロック実行の実行時間は2秒になります。
両方のアクションを別々のスレッドで実行し、メインスレッドでそれらの結果を待つことができればもっと良いでしょう。
-
2つの別々のスレッドで同時に処理を開始することで、
async()
コルーチンを利用してこれを実現できます。
@Test
fun givenHaveTwoExpensiveAction__whenExecuteThemAsync__thenTheyShouldRunConcurrently() {
runBlocking<Unit> {
val delay = 1000L
val time = measureTimeMillis {
//given
val one = async(CommonPool) {
someExpensiveComputation(delay)
}
val two = async(CommonPool) {
someExpensiveComputation(delay)
}
//when
runBlocking {
one.await()
two.await()
}
}
//then
assertTrue(time < delay ** 2)
}
}
2つの高価な計算を送信した後、
runBlocking()
呼び出しを実行してコルーチンを中断します。結果
one
と
two
が使用可能になると、コルーチンが再開され、結果が返されます。
この方法で2つのタスクを実行すると、約1秒かかります。
async()
メソッドの2番目の引数として
CoroutineStart.LAZY
を渡すことができますが、これは非同期計算が要求されるまで開始されないことを意味します。
runBlocking
コルーチンで計算を要求しているので、
two.await()
の呼び出しは
one.await()
が終了した後にのみ行われます。
@Test
fun givenTwoExpensiveAction__whenExecuteThemLazy__thenTheyShouldNotConcurrently() {
runBlocking<Unit> {
val delay = 1000L
val time = measureTimeMillis {
//given
val one
= async(CommonPool, CoroutineStart.LAZY) {
someExpensiveComputation(delay)
}
val two
= async(CommonPool, CoroutineStart.LAZY) {
someExpensiveComputation(delay)
}
//when
runBlocking {
one.await()
two.await()
}
}
//then
assertTrue(time > delay ** 2)
}
}
-
この特定の例では、実行が遅れるとコードが同期的に実行されます**
await()
を呼び出すと、メインスレッドがブロックされ、task
one
がtask
two
を終了した後にのみトリガされるためです。
私たちは、非同期のアクションがブロック的な方法で実行される可能性があるので、怠惰な方法で非同期のアクションを実行することに注意する必要があります。
8結論
この記事では、Kotlinコルーチンの基本を調べました。
buildSequence
がすべてのコルーチンの主要な構成要素であることがわかりました。この継続受渡しプログラミングスタイルでの実行の流れがどのように見えるかを説明しました。
最後に、非同期プログラムを作成するための非常に便利な構成要素が多数含まれている
kotlinx-coroutines
ライブラリを調べました。
これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubプロジェクト]にあります – これはMavenプロジェクトなので、簡単にできます。そのままインポートして実行します。