Help us understand the problem. What is going on with this article?

Kotlin 1.3のCoroutineのコンテキストとディスパッチャ⑧(明示的なJob経由でキャンセル)

検証環境

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

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

準備

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

Cancellation via explicit job

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

いつもだったらタイトルから内容を予想していました。
しかし、今回はタイトルの考察はしません。
なので、今回は内容を理解した上でタイトルの意味を考えてみます。

それでは、読み進めていきます。

Let us put our knowledge about contexts, children and jobs together. Assume that our application has an object with a lifecycle, but that object is not a coroutine. For example, we are writing an Android application and launch various coroutines in the context of an Android activity to perform asynchronous operations to fetch and update data, do animations, etc. All of these coroutines must be cancelled when activity is destroyed to avoid memory leaks.

意訳込みですが、(略)

コンテキストや子コルーチンやジョブについてまとめてみましょう。
例えば、開発するアプリがライフサイクルを持っていたとします。かつ、コルーチンではないとします。そのアプリがAndroidアプリだった場合、Activityのコンテキストで様々なコルーチンを起動し、そのコルーチンで非同期処理や最新データの取得、アニメーション処理などを実行します。それらのコルーチンは、Activityが終了(アプリが終了し、メモリ上から破棄される)の際に全てキャンセルしなければなりません。キャンセルしないと、メモリーリークが発生します。

私もAndroidアプリなどを開発するので、アプリが終わるときなどは必ずバックグラウンド処理はキャンセルします。
確かにコルーチンも軽量なスレッドなので、キャンセルが必要ですね。
続けて読み進めていきます。

We manage a lifecycle of our coroutines by creating an instance of Job that is tied to the lifecycle of our activity. A job instance is created using Job() factory function when activity is created and it is cancelled when an activity is destroyed like this:

訳します

開発者はActivityのライフサイクルに紐付いたJobのインスタンスで、ライフサイクルで使用しているコルーチンを管理します。Activityが生成されるタイミングで、JobのインスタンスをJob Factoryで生成します。そして、Activityがメモリ上から削除されるタイミングでキャンセルしています。
サンプルコードはいかのとおりです。

Androidアプリを開発している人にはわかりやすいですが、Androidでは使っていない人にとっては意味不明だと思います。
コールバックという考え方があり、インスタンスが生成されるタイミングやメモリ上から破棄されるタイミングで呼び出されるメソッドが存在します。そのようなメソッドをライフサイクルメソッドなどと呼ばれます。おおくの場合は、ライフサイクルメソッドをオーバーライドして、それらのタイミングで実行したい処理を記述します。

早速、サンプルコードを確認してみます。

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }
// to be continued ...

createメソッドでJobのオブジェクトを生成して、destroyメソッドでキャンセルしているようです。

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

We also implement CoroutineScope interface in this Actvity class. We only need to provide an override for its CoroutineScope.coroutineContext property to specify the context for coroutines launched in its scope. We combine the desired dispatcher (we used Dispatchers.Default in this example) and a job:

訳します

ActivityクラスにCoroutineScopeを実装しています。起動したコルーチンのコンテキストを扱うために、CoroutineScope.coroutineContext プロパティをオーバーライドしています。今回のサンプルコードでは、ディスパッチャーのデフォルトとジョブを結合しています。コードを確認してください。

なるほど、プロパティのオーバーライドをすることにより、アクセスを用意にするコードがあるみたいです。確認してみましょう。公式サイトではコードの断片だけですが、コンパイルが通るように少し変更しています。

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job
}

確かに、coroutineContextのゲッターメソッドでデフォルトとジョブを結合していますね。

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

Now, we can launch coroutines in the scope of this Activity without having to explicitly specify their context. For the demo, we launch ten coroutines that delay for a different time:

訳します。

このように記述することにより、明示的にコンテキストをしてすることなく、オーバーライドしたコンテキストを使用してコルーチンが起動します。サンプルコードのdoSomethingメソッドでは、delay関数に渡す時間がことなるコルーチンを10回きどうしています。

確かに、プロパティをオーバーライドすれば、指定しなくてもいけそうですね。
サンプルコードも見てみましょう。

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job

    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
}

repeat関数のカウンターと2000を乗算してそれをdelay関数に渡してますね。

ここから、実行に関する説明みたいです。ここまでが長かったですが、実行まで後少しです。
読み進めましょう。

In our main function we create activity, call our test doSomething function, and destroy activity after 500ms. This cancels all the coroutines that were launched which we can confirm by noting that it does not print onto the screen anymore if we wait:

訳します

main関数では、Activityのオブジェクトを生成し、doSomethingメソッドを呼び出します。そして、500ミリ秒後、destoryメソッドを呼び出します。destroy関数を呼び出すことにより、全てのコルーチンがキャンセルされます。標準出力を確認しているとrepeat関数が動作せず、何も表示されないことが確認できます。

なるほど、createメソッドで生成したコンテキストに紐付いているから、キャンセルすると全てのコルーチンのキャンセル待ちをするということでしょうか?
サンプルコードも確認してみましょう。

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }
    // to be continued ...

    // class Activity continues
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job
    // to be continued ...

    // class Activity continues
    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.create() // create an activity
    activity.doSomething() // run test function
    println("Launched coroutines")
    delay(500L) // delay for half a second
    println("Destroying activity!")
    activity.destroy() // cancels all coroutines
    delay(1000) // visually confirm that they don't work
}

説明にあったとおりの内容ですね。
実行してみました。

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

確かにキャンセルされていることが確認できます。

最後にタイトルを考えてみると、ライフサイクルをもつ場合は、プロパティなどでJobを保持する必要があるってことで、結果的に明示的なJobでキャンセル処理が行われているってことを表していたんですね。

まとめ

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

  • ライフサイクルを持つようなアプリの場合は、Jobをプロパティとして保持する必要がある
naoi
casareal
システム開発/評価・検証支援/品質改善支援サービスと現場に即した実践的なIT研修サービスを提供しています。
https://www.casareal.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした