Kotlin:コルーチン(Coroutine)

投稿日:  更新日:

近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。

サクサク動作するアプリを作るために、この恩恵を使わなければ損です。

となると、必然的に非同期処理(マルチスレッド)を使うことになります。

JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。

Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。

今回は、このコルーチンについて、まとめます。

スポンサーリンク

コルーチンとは

非同期処理(マルチスレッド)

アプリケーションはメインスレッド(UIスレッド)が実行します。

実行される処理の内訳をもう少し具体的に示すと、次のようなものです。

【処理の内訳】
  ・Activityの処理
     onCreate~onResume、onPause~onDestroy
  ・画面のリフレッシュ(再描画)
     1/60[s]間隔(フレームレート:60[fps]が目標)
  ・タッチスクリーンのエベント処理
     クリック、ドラッグ、スワイプ、など

メインスレッドで実行される処理の内訳

ここに上げた処理は、アプリケーションの実行で最低限必要なものです。

アプリケーションを作成していると、たびたび重い処理(長時間の処理)に出合います。

メインスレッドで重い処理を実行してしまうと、最低限必要な処理が実行できません。

非同期処理

画面のリフレッシュができなくなれば、滑らかに動くアニメーション効果は得られません。

また、タッチスクリーンのエベント処理が遅れてしまえば、リアルタイムな操作感が失われて、もっさりとした動作になってしまいます。

これを回避するために、メインスレッドと並列に動作するワーカースレッドを起動して、ワーカースレッド側に重い処理を担当させる方法を取ります。

このような方法を非同期処理(マルチスレッド)と言います。

コルーチン(Coroutine)

コルーチン(Coroutine)は「非同期処理の手法」の1つです。

Kotlinが提供します。

他にAndroidで利用可能な非同期処理の手法として、JavaのThreadクラスやAndroid APIのAsyncTaskクラスがあります。

それらと比べて、コルーチンは次の特徴を持ちます。

 (1)スレッドをブロックせずに処理を一時停止・再開できる
      ⇒non-blocking動作の効果
 (2)非同期処理を同期処理のように記述できる
      ⇒Suspend関数の効果
 (3)起動のオーバヘッド・メモリー使用量が小さい
      ⇒スレッドプールの効果

Android APIのAsyncTaskクラスは≧API 30で非推奨になりました。代わりにコルーチンの使用が推奨されています。

スポンサーリンク

環境設定

コルーチンで非同期処理を行うには、ライブラリが必要になります。

ライブラリの依存リスト(dependencies)に次の一行を追加します。

dependencies {
    ....
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
    ...
}
スポンサーリンク

コル―チンの例

コルーチンを使った非同期処理の基本的な例を示して解説します。

    var scope: CoroutineScope? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            scope = SampleScope()
            scope?.launch() {
                /* --- ここから ---                        */
				/* タスクブロック:重い処理(長時間の処理)*/
                println("Thread = ${Thread.currentThread().name}")
				/* --- ここまで ---                        */
            }
        }
	}
		
    override fun onPause() {
        ...
        scope?.cancel()
    }
Thread = DefaultDispatcher-worker-2    // スレッド名

図のように、ワーカースレッドが起動されて、タスクブロックが実行されます。

コルーチンの例

スポンサーリンク

(解説1)CoroutineScopeはコルーチンの起点

CoroutineScope(サンプルのSampleScope)はコルーチンの起点です。

コルーチンの開始で必要になります。

CoroutineScope(サンプルのSampleScope)はCoroutineScopeインターフェースを実装したものです。実装には抽象プロパティCoroutineContextの定義が必要になります。

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScopeの構成

CoroutineContextは図にあるような属性値を持つので必要に応じて指定します。

class SampleScope : CoroutineScope {
    override val coroutineContext: CoroutineContext =
        Job() + Dispatchers.Default + CoroutineName("Hoge") // Contextを定義
}

また、CoroutineScopeは表のような拡張関数を持ちます。これらは起点としての役割を果たすために必要な関数群です。

【CoroutineScopeの拡張関数】

関数名概要コメント
launchコルーチンを開始する
(スレッドを起動してタスクを実行)
タスクブロックの戻り値なし
Builderと呼ばれる
起動したスレッドのJobを返す
asyncコルーチンを開始する
(スレッドを起動してタスクを実行)
タスクブロックの戻り値あり
Builderと呼ばれる
起動したスレッドのDeferredを返す
isActiveコルーチンの状態を返すCoroutineContext内のJob属性を取り出して、Job#isActive( )を実行
cancelコルーチンの実行をキャンセルCoroutineContext内のJob属性を取り出して、Job#cancel( )を実行
拡張関数の詳細
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
}
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
public val CoroutineScope.isActive: Boolean
    get() = coroutineContext[Job]?.isActive ?: true
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled ...")
    job.cancel(cause)
}
スポンサーリンク

(解説2)CoroutineContextはコルーチンの設計図

CoroutineContextはコルーチンの属性を格納した箱です。

【CoroutineContextが格納する属性】

属性名Key概要
JobJob自身が管理しているスレッドのハンドラー
DispatchersContinuationInterceptorスレッドの取得先(スレッドプール名)
NameCoroutineNameコルーチンの名前(デバッグ用)
ExceptionHandlerCoroutineExceptionHandler例外通知のハンドラー
※スレッドのハンドラー:スレッドに対して指示を出す場合の窓口

コルーチンで起動されるスレッドはCoroutineContextの内容に従います。コルーチンの設計図と言えるものです。

サンプルのSampleScopeはCoroutineContextを以下のように定義しています。

   Job
   Displatchers
   Name
   ExceptionHandler
:Jobクラスの新規インスタンス
:Dispatchers.Default ⇒ Defaultプールから取得
:”Hoge”
:Null ⇒ 指定なし

各々の属性を参照したければ、次のようにKeyを指定して取り出すことが出来ます。

        val _job: Job? = context[Job]
        val _dispacher: ContinuationInterceptor? = context[ContinuationInterceptor]
        val _name: CoroutineName? =	context[CoroutineName]
        val _exceptionHandler: CoroutineExceptionHandler? =	context[CoroutineExceptionHandler]
スポンサーリンク

(解説3)CoroutineContextの加算

図は属性のクラス図です。属性はすべてCoroutineContextを継承していることが分かります。

CoroutineContextのクラス図

〔↑↑画像のクリックで拡大↑↑〕

つまり、Job属性とはJob値のみをもったCoroutineContextであり、Dispatchers属性とはDispatchers値のみをもったCroutineContextです。

CoroutineContextはplusオペレータが再定義されているので「+」演算子が使用できます。ただし、算術的な「+」ではなく、上書きに近い動作です。

CoroutineACoroutineBCoroutineA + Bコメント
nullnullnullnullのまま
ContextA[Key]nullContextA[Key]nullで上書きしない
nullContextB[Key]ContextB[Key]Bの値で上書きする
ContextA[Key]ContextB[Key]ContextB[Key]
※ContextA[Key]、ContextB[Key]:各属性を表す

このplusオペレータにより、次のような加算が許されています。

val coroutineContext: CoroutineContext = 
                Job() + Dispatchers.Default + CoroutineName("Hoge")
スポンサーリンク

(解説4)ビルダー(launch)でコルーチンを開始

CoroutineScope#launch(拡張関数)を使ってコルーチンを開始するとともに、スレッドを起動します。タスクブロックはスレッドで処理されます。

このlaunchのことをビルダー(Builder)と呼びます。

図のように、起動されたスレッドはCoroutineScopeのインスタンスを持ち、thisにより参照が出来ます。Job属性だけが現在のスレッドのJob(色で区別)である点に注意してください。

launchでスレッドを起動

また、launchを使ってスレッドを起動すると、起動元(launchを実行した)と起動先(launchで起動された)スレッドの間に親子関係が生まれます。

この親子関係の情報がCoroutineContextのJobへ書き加えられます。

スレッドの親子関係の情報

スポンサーリンク

(解説5)スレッドはスレッドプールから取得

コルーチンはスレッドプールを持っています。スレッドプールとはスレッドを溜めておく場所です。

launchでスレッドを起動するとき、スレッドはスレッドプールから取得されます。スレッドプールにスレッドが無い場合は、スレッドを新規作成してから取得します。

スレッドの処理が終了したり一時停止(Suspend)されたりした場合、スレッドはスレッドプールへ返却されます。

コルーチンのスレッドプール

CoroutineContextのDispatchersはスレッドプールを指定する属性です。ビルダーはDispatchersで指定されたプールからスレッドを取得します。

各々のスレッドプールは管理できる最大のスレッド数があるので注意してください。

【Dispatchers属性】

プール名
Dispatchers.XXX
概要最大数
Mainメインスレッドが割り当てられる
UI処理を実行する場合に適する
スレッドがアクティブな場合はアイドルになるまで待機
Default共有プールからワーカースレッドが割り当てられる
負荷の高い作業を実行する場合に適する
スレッドが不足している場合は空きが出るまで待機
コア数
IO共有プールからワーカースレッドが割り当てられる
ディスク、ネットワークなどの I/O処理を実行する場合に適する
スレッドが不足している場合は空きが出るまで待機
64 (Default)
Unconfined特定のスレッドに制限されない
初回はBuilderを実行したスレッドと同じプールのスレッドが割り当てられる、その後の再取得はそれぞれ異なる
スポンサーリンク

(解説6)キャンセルのハンドリング

コルーチンの処理を途中でキャンセルする場合は、CoroutineScope#cancel(拡張関数)を実行します。

CoroutineScope#cancelの実態は、coroutineContextからスレッドのハンドラであるJob属性を取り出して、Job#cancelを実行することです。

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled ...")
    job.cancel(cause)
}

このJob#cancelの実行はキャンセルの有無を示すフラグが立つだけで、ハンドリング(処理の中断≒スレッドの強制終了)を行いません。

ハンドリングはアプリのプログラム側で行う必要があります。

以下の例は、コルーチンの状態をCoroutineScope#isActive(拡張関数)で参照して、キャンセルされている場合に処理の中断を行っています。

...
scope?.launch() {
    // ---- ここから 重い処理(時間の長い処理)
	// isActive:true  キャンセルされていない
	//         :false キャンセルされている
	while(left > 0 && this.isActive){ // 処理の中断or継続
	    //
	    // いろいろな処理
		//
		left--
	}
	// ---- ここまで
}
...
スポンサーリンク

(解説7)キャンセルの伝搬

Job#cancelの実行はキャンセルの有無を示すフラグを立たせます。

このフラグはJobの親から子へ伝搬するという重要な特徴を持ちます。子から親へ伝搬することはありません。

フラグの伝搬したスレッドは全て強制終了されることになります。

キャンセルの伝搬

コルーチンの起点のCoroutineScope(サンプルのSampleScope)でCoroutineScope#cancelを実行すれば、起点由来のスレッドは全て強制終了されます。つまり、コルーチン全体がキャンセルされることを意味します。

スポンサーリンク

(解説8)コルーチンの生存期間

CoroutineScope#cancelの発行でコルーチン全外がキャンセルされます。これをライフサイクルと関連付ければ、コルーチンの生存期間を設定できます。

以下の例は、Acitivityのライフサイクルと関連付けています。

    var scope: CoroutineScope? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            scope = SampleScope()
            scope?.launch() {...}
        }
	}
	
	override fun onResume() {
        ...
    }
		
    override fun onPause() {
        ...
        scope?.cancel()
    }

Activityの表示されている期間がコルーチンの生存期間です。なぜなら、onPauseでコルーチン全体のキャンセルを行っているからです。

コルーチンの生存期間

このようにコルーチンに生存期間を与えると、期間外はコルーチンが動いていないことを保証できます。

意図に反してコルーチンが動き続けてしまうような、コルーチンのメモリーリークを防ぎます。その結果、アプリの安定性を高めるという効果があります。

スポンサーリンク

関連記事:

Kotlinは関数を変数に代入したり、引数で受け渡したりできます。 関数を第一級オブジェクトで扱えるためです。 これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。 以上のような操作で必要となる「関数オブジェクト」について、まとめます。 ...
クロージャ(Closure)は新しい概念ではなく、関数型プログラミングに古くから存在していました。 手続き型やオブジェクト指向プログラミングにも、だいぶ前から採用が進んでいます。 Kotlinもクロージャが使える言語の1つです。 ...
Androidのアプリを作成しているとSAM変換が頻繁に登場します。 プログラムが簡素に記述できることから、プログラマーに好評なようです。 Kotlinが関数を第一級オブジェクトとして扱える恩恵です。 しかし、私はSAM変換に出合うと、いつもプログラムの論理が把握できなくて戸惑います。なので、苦手です。あまり好きではありません。 苦手を克服するために、もっとSAM変換の理解を深めたいと思い、まとめてみました。 ...
Android Studio Giraffe(2023.07)になって、ビルドスクリプトの推奨がKotlin DSLになりました。 この機会に、ビルドスクリプトと記述言語についてまとめます。 ※環境:Android Studio Giraffe | 2022.3.1    :Android Gradle Plugin 8.1.0    :Gradle 8.0 ...
Kotlinにおいて、各クラスのインスタンスに自動生成されるequals()関数は、classとdata classで比較対象に違いがあります。 紛らさしいので、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
KotlinはJavaのプリミティブ型に当たるデータ型を持ちません。しかし、同等な記述が基本データ型を用いて出来ます。 Kotlinにおいて、基本データ型がどのように扱われているのか、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。 何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。 ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 1     Kotlin 1.8.10 ...
スコープ関数はKotlinの標準ライブラリで提供されています。 そのことから、Kotlinを特徴付ける重要な構文であることが分かります。 ただし、「プログラムの動作を定義する構文」ではなく、「プログラムのコードの品質を上げる構文」です。 同様な動作はスコープ関数を使わなくても記述できます。しかし、スコープ関数を使えばスマートな(洗練された)記述になります。 積極的に使いたいと思いますが、applyとlet以外はあまり使う機会がありません。また、稀にwithなどが登場すると、コードの流れが理解できずに戸惑います。 ですので、スコープ関数について、まとめました。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1 ...
スポンサーリンク