Kotlin
coroutine

Kotlin 1.3のCoroutineの使い方④(構造化されている並列処理)

検証環境

この記事の内容は、以下の環境で検証しました。

  • Intellij IDEA ULTIMATE 2018.2
  • Kotlin 1.3.0
  • Gradle Projectで作成
  • GradleファイルはKotlinで記述(KotlinでDSL)

準備

詳細は下記の準備を参照してください。
https://qiita.com/naoi/items/8abf2cddfc2cb3802daa

Structured concurrency

前回に引き続き、公式サイトを読み解いていきます。

今回のタイトルを翻訳すると意味不明な感じになります。
公式サイトの説明文も長いので、少しずつ理解していきます。

早速、説明を読んでみましょう。

There is still something to be desired for practical usage of coroutines.
When we use GlobalScope.launch we create a top-level coroutine.
Even though it is light-weight, it still consumes some memory resources while it runs.
If we forget to keep a reference to the newly launched coroutine it still runs. What if the code in the coroutine hangs (for example, we erroneously delay for too long), what if we launched too many coroutines and ran out of memory? Having to manually keep a reference to all the launched coroutines and join them is error-prone.

意訳込みですが、このように記述されています。

GlobalScope.launchを使うと、トップレベルのコルーチンが作成されます。
コルーチンは軽量なスレッドですが、少なからずメモリは消費します。もし、Jobのオブジェクトを取得しわすれてしまい。ずっと動き続けてしまったり、大きく遅延してしまった場合はどうなるでしょう。また、多くののコルーチンを手動で管理してしまうと、エラーの発生の要因になりかねません。このままでコルーチンを利用するには、あまりにも実用的ではないです。

なるほど、確かに現状ではマルチスレッドの管理と同じように手間がかかってしまい、実用的ではないです。

続けて、読み進めていきます。

There is a better solution. We can use structured concurrency in our code. Instead of launching coroutines in the GlobalScope, just like we usually do with threads (threads are always global), we can launch coroutines in the specific scope of the operation we are performing.

意訳込みですが(以下略)

良い解決策があります。
Kotlinではコルーチンを構造化し、並列処理が可能です
GlobalScopeでコルーチンを起動するのではなく、通常のスレッドみたいに起動します。(注意:通常のスレッドはglobal扱いです。あくまでイメージです。)
実行範囲を特定して、コルーチンを実行すれば構造化ができます。

たしかにGlobalでは範囲が広すぎますね。あと、構造化と並列処理のタイトルの意味がわかりました。コルーチンそのものを構造化して、並列処理で実行するってことですね。

では、更に読み進めていきます。

In our example, we have main function that is turned into a coroutine using runBlocking coroutine builder. Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine (runBlocking in our example) does not complete until all the coroutines launched in its scope complete. Thus, we can make our example simpler:

意訳込(以下略)

この例では、runBlockingコルーチンビルダーを使用しているため、全てのコルーチンはCoroutineScopeに追加されます。
また、このコルーチンの外側のコルーチンはrunBlockingが完了するまで、外側のコルーチンは処理を完了できません。
runBlocking内にコルーチンを閉じて実行する事が可能になります。

コードも併せて見ないと理解が出来なさそうなので、コードも確認してみます。

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {

    launch { 
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

前回と大きく変わったところというと、 GlobalScope.launchlaunch になっていることでしょうか。
これでCoroutineScopeの範囲になるのか不安なので、launchの実装を確認してみます。
以下のようなコードになっていました。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

コードを確認すると、確かにCoroutineScopeで実行されていますね。
ということは、このブロックで言いたかったことを図にするとこの様になると思います。

structured coroutine.png

まとめ

このブロックで理解できたことは以下のことだと思います。

  • GlobalScopeでコルーチンを生成しない
  • Coroutineスコープでコルーチンを生成する
  • launch関数でCoroutineスコープのコルーチンを生成できる