KotlinのCoroutineを使ってみたい

2020-05-04 Kotlin coroutine

はじめに

公式とGoogle Codelabsを参考にKotlinのcoroutineについて学習したので
メモとして残しておきます。

環境

  • Windows 10 Home (1903)

  • kotlin (1.3.72)

  • kotlinx-coroutines-core (1.3.4)

Your first coroutine with Kotlin

  • coroutineはkotlinの言語レベルでサポートされている非同期プログラミングなどを実行する新しい手法(threadのような実行単位の1つと解釈した)

  • coroutineはthreadの共有プール上で実行され、並列に処理される

  • 1つのthreadで多くのcoroutineを実行することができるのでthreadを生成するよりコストが低い

  • coroutineはthreadをブロックせずにcoroutineのみをsuspend/resumeできる

  • threadをブロックせず、coroutine内から呼び出す関数にはsuspend修飾子を付ける

  • suspend関数はcoroutineかsuspend関数からのみ呼び出すことができる

val deferred = (1..1_000_000).map { n ->
    GlobalScope.async {
        delay(1000) // (1)
        n
    }
}

runBlocking {
    val sum = deferred.map { it.await().toLong() }.sum() // (2)
    println("Sum: $sum")
}
  1. coroutineを1,000msec停止する(≒1,000,000sec実行スレッドでかかるわけではない)

  2. coroutineの結果を待ち合わせる。10sec程で完了するのでcoroutineが並列処理されているのがわかる

Asynchronous Programming Techniques

Threading

  • context switchingが発生するためコストが高い

  • 競合によりデバッグが難しい

Callbacks

  • callbackがネストしていくことで可読性が低下する

  • エラー処理が複雑になる

Futures, Promises et. al

  • 戻り値はPromise型になり、実際のデータを直接指定することができない

  • エラー処理が複雑になる(エラーの伝搬方法が直感的ではない気がする)

Reactive Extensions

  • 多くのプラットフォームに移植されているので、一貫したAPIを利用できる

  • 新しい概念を導入することになるのでコストは高くなる

Coroutines

  • 同期コードを書くのと同様にtop-downで記述できる

  • エラー処理は既存のプログラミング方法と変える必要がない

Introduction to Coroutines and Channels

solutionブランチのコードを見ながらざっと流し読みした。

  • coroutine scopeとcoroutine contextという概念がある

  • coroutine scopeは構造化されており親子関係を持つ

  • 同じcoroutine scopeで実施されている場合、親coroutineがキャンセルされると子coroutineも自動的にキャンセルされる

  • channelを使用して別々のcoroutine間で通信を行うことができる

Use Kotlin Coroutines in your Android App

  • suspend関数はコードが実行されるthreadを指定する訳ではないのでmain threadでも実行できる

  • androidxのlifecycle-viewmodel-ktxライブラリにはDispathcers.MainにバインドされViewModelがクリアされた時にキャンセルされるviewModelScopeが追加されている

  • coroutineのUnit Testはcoroutineのタイマー操作を行うことがdelayさせた時間を早めて実行することができ、実際にdelayさせる時間分待つ必要はない

  • launchやasyncを使ってcoroutineを開始できる

  • 結果を返す必要がない場合はlaunchを使い、結果を返す必要がある場合はasyncを使う

  • 子coroutineが例外を投げた場合、デフォルトで親coroutineもキャンセルされる

  • CoroutineExceptionHandlerを使ってcoroutine内でcatchされなかった例外が発生した時の動作をカスタマイズできる

  • IO dispatcherはネットワークやディスクからの読み込みといったIOに最適化されている

  • Default dispatcherはCPUを集中的に使用するタスクに最適化されている

  • RoomもRetrofitもsuspend関数をmain-safeにしているので、Dispathcers.Mainから呼び出しても安全(main threadをブロックしない)

  • main-safeなsuspend関数であればDispatcher.Mainから呼び出せる(withContextを使う 必要がない)

  • 慣習として自分で用意したsuspend関数はmain-safeであることを保証する

  • テスト関数を抜けた時点でテストが終了してしまうため、coroutineが完了するまでテスト実行threadが待つようにrunBlockingTestでcoroutineを開始する必要がある

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}
  • refreshTitleが実行されたかどうかを知らずにテストを終了してしまう

  • refreshTitleが例外を投げた場合、テストコールスタックには投げられず、GlobalScopeの例外ハンドラでuncaught exceptionとして扱われる

  • withTimeout functionを使用することでcoroutineのタイムアウトを設定できる

  • advanceTimeBy functionを使用することでcoroutineの仮想時間を指定した時間だけ勧めることができるので、タイムアウトのテストに利用できる

感想

Coroutineを利用することでアプリのパフォーマンスが向上するだけでなく
可読性も大幅に上がるので、これを機に利用していこうと思います。