近頃の携帯端末はクワッドコア(プロセッサが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 }
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( )を実行 |
(解説2)CoroutineContextはコルーチンの設計図
CoroutineContextはコルーチンの属性を格納した箱です。
【CoroutineContextが格納する属性】
属性名 | Key | 概要 |
---|---|---|
Job | Job | 自身が管理しているスレッドのハンドラー |
Dispatchers | ContinuationInterceptor | スレッドの取得先(スレッドプール名) |
Name | CoroutineName | コルーチンの名前(デバッグ用) |
ExceptionHandler | CoroutineExceptionHandler | 例外通知のハンドラー |
※スレッドのハンドラー:スレッドに対して指示を出す場合の窓口 |
コルーチンで起動されるスレッドはCoroutineContextの内容に従います。コルーチンの設計図と言えるものです。
サンプルのSampleScopeはCoroutineContextを以下のように定義しています。
Displatchers
Name
ExceptionHandler
: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を継承していることが分かります。
つまり、Job属性とはJob値のみをもったCoroutineContextであり、Dispatchers属性とはDispatchers値のみをもったCroutineContextです。
CoroutineContextはplusオペレータが再定義されているので「+」演算子が使用できます。ただし、算術的な「+」ではなく、上書きに近い動作です。
CoroutineA | CoroutineB | CoroutineA + B | コメント |
---|---|---|---|
null | null | null | nullのまま |
ContextA[Key] | null | ContextA[Key] | nullで上書きしない |
null | ContextB[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で起動された)スレッドの間に親子関係が生まれます。
この親子関係の情報がCoroutineContextのJobへ書き加えられます。
(解説5)スレッドはスレッドプールから取得
コルーチンはスレッドプールを持っています。スレッドプールとはスレッドを溜めておく場所です。
launchでスレッドを起動するとき、スレッドはスレッドプールから取得されます。スレッドプールにスレッドが無い場合は、スレッドを新規作成してから取得します。
スレッドの処理が終了したり一時停止(Suspend)されたりした場合、スレッドはスレッドプールへ返却されます。
CoroutineContextのDispatchersはスレッドプールを指定する属性です。ビルダーはDispatchersで指定されたプールからスレッドを取得します。
各々のスレッドプールは管理できる最大のスレッド数があるので注意してください。
【Dispatchers属性】
プール名 Dispatchers.XXX | 概要 | 最大数 |
---|---|---|
Main | メインスレッドが割り当てられる UI処理を実行する場合に適する スレッドがアクティブな場合はアイドルになるまで待機 | 1 |
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でコルーチン全体のキャンセルを行っているからです。
このようにコルーチンに生存期間を与えると、期間外はコルーチンが動いていないことを保証できます。
意図に反してコルーチンが動き続けてしまうような、コルーチンのメモリーリークを防ぎます。その結果、アプリの安定性を高めるという効果があります。
関連記事: