コトリンコルーチン入門

コトリンコルーチンの概要

1. 概要

この記事では、Kotlin言語のコルーチンについて説明します。 簡単に言えば、coroutines allow us to create asynchronous programs in a very fluent wayであり、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コルーチンを一時停止できます。

Suspending functions can be created as standard Kotlin functions, but we need to be aware that we can only call them from within a coroutine.そうしないと、コンパイラエラーが発生します。

buildSequence,内で通話を一時停止した場合、その通話はステートマシンの専用状態に変換されます。 コルーチンは、他の関数と同様に、変数に渡して割り当てることができます。

fibonacciSeqコルーチンには、2つの中断点があります。 1つ目はyield(1)を呼び出すとき、2つ目は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リポジトリも追加する必要があることに注意してください。


    org.jetbrains.kotlinx
    kotlinx-coroutines-core
    0.16



    
        central
        http://jcenter.bintray.com
     

4. launch() Coroutineを使用した非同期プログラミング

kotlinx-coroutinesライブラリは、非同期プログラムの作成を可能にする多くの便利な構造を追加します。 入力リストにStringを追加する高価な計算関数があるとしましょう。

suspend fun expensiveComputation(res: MutableList) {
    delay(1000L)
    res.add("word!")
}

そのサスペンド関数を非ブロッキング方式で実行するlaunchコルーチンを使用できます。これには、引数としてスレッドプールを渡す必要があります。

launch関数は、join()メソッドを呼び出して結果を待機できるJobインスタンスを返します。

@Test
fun givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay() {
    // given
    val res = mutableListOf()

    // when
    runBlocking {
        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. コルーチンは非常に軽量です

100000の操作を非同期で実行したい状況を想像してみましょう。 このような多数のスレッドを生成すると、非常にコストがかかり、OutOfMemoryException.が生成​​される可能性があります。

幸いなことに、コルーチンを使用する場合、これは当てはまりません。 必要な数のブロック操作を実行できます。 内部では、これらの操作は、スレッドを過剰に作成することなく、固定数のスレッドによって処理されます。

@Test
fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory() {
    runBlocking {
        // 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)
    }
}

100,000個のコルーチンを実行しており、実行するたびに大幅な遅延が発生することに注意してください。 それでも、これらの操作はCommonPool.のスレッドを使用して非同期で実行されるため、あまり多くのスレッドを作成する必要はありません。

6. キャンセルとタイムアウト

長時間実行される非同期計算をトリガーした後、結果に関心がなくなったためにキャンセルしたい場合があります。

launch()コルーチンを使用して非同期アクションを開始すると、isActiveフラグを調べることができます。 このフラグは、メインスレッドがJob:のインスタンスでcancel()メソッドを呼び出すたびにfalseに設定されます

@Test
fun givenCancellableJob_whenRequestForCancel_thenShouldQuit() {
    runBlocking {
        // given
        val job = launch(CommonPool) {
            while (isActive) {
                println("is working")
            }
        }

        delay(1300L)

        // when
        job.cancel()

        // then cancel successfully

    }
}

これは非常にエレガントでeasy way to use the cancellation mechanismです。 非同期アクションでは、isActiveフラグがfalseと等しいかどうかを確認し、処理をキャンセルするだけで済みます。

処理をリクエストしていて、その計算にかかる時間がわからない場合は、そのようなアクションにタイムアウトを設定することをお勧めします。 指定されたタイムアウト内に処理が完了しない場合、例外が発生し、適切に対応できます。

たとえば、アクションを再試行できます。

@Test(expected = CancellationException::class)
fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut() {
    runBlocking {
        withTimeout(1300L) {
            repeat(1000) { i ->
                println("Some expensive computation $i ...")
                delay(500L)
            }
        }
    }
}

タイムアウトを定義しないと、計算がハングするため、スレッドが永久にブロックされる可能性があります。 タイムアウトが定義されていない場合、コードでそのケースを処理できません。

7. 非同期アクションの同時実行

2つの非同期アクションを同時に開始し、その後それらの結果を待つ必要があるとしましょう。 処理に1秒かかり、その処理を2回実行する必要がある場合、同期ブロック実行の実行時間は2秒になります。

これらのアクションの両方を別々のスレッドで実行し、メインスレッドでそれらの結果を待つことができればより良いでしょう。

2つの別々のスレッドで同時に処理を開始することによるWe can leverage the async() coroutine to achieve this

@Test
fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently() {
    runBlocking {
        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()呼び出しを実行してコルーチンを一時停止します。 結果onetwoが使用可能になると、コルーチンが再開され、結果が返されます。 この方法で2つのタスクを実行するには、約1秒かかります。

async()メソッドの2番目の引数としてCoroutineStart.LAZYを渡すことができますが、これは、要求されるまで非同期計算が開始されないことを意味します。 runBlockingコルーチンで計算を要求しているため、two.await()の呼び出しは、one.await()が終了したときにのみ行われることを意味します。

@Test
fun givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently() {
    runBlocking {
        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)
    }
}

The laziness of the execution in this particular example causes our code to run synchronously.これは、await()を呼び出すとメインスレッドがブロックされ、タスクoneが終了した後にのみタスクtwoがトリガーされるために発生します。

非同期アクションはブロッキング方式で実行される可能性があるため、怠actionsな方法で非同期アクションを実行することに注意する必要があります。

8. 結論

この記事では、Kotlinコルーチンの基本について説明しました。

buildSequenceがすべてのコルーチンの主要な構成要素であることがわかりました。 この継続渡しプログラミングスタイルの実行フローがどのように見えるかを説明しました。

最後に、非同期プログラムを作成するための非常に便利な構造を多数備えているkotlinx-coroutinesライブラリを調べました。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。