開発者ドキュメント

コトリンコルーチン入門


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プロジェクトなので、簡単にできます。そのままインポートして実行します。

モバイルバージョンを終了