内容
自分がKotlin-Coroutinesを雰囲気で使っていたので調べたメモ。
Coroutineとは
Coroutineとは軽量のThreadであり、非同期処理やノンブロッキング処理を行うためのアプローチの一つ。
軽量なスレッドとはどういうことか
以下のような違いがあるよう。
※理解にはシステムアーキテクチャの知識が若干必要なので、ここは無視でいいかもしれない。
-
Threadはシステム的に以下の特性を持っている
- Thread一つにつきプロセス上に対象Thread専用のメモリリソースを確保する必要がある
- 実行Threadをシステムスケジューラが切り替えるのでオーバーヘッドが発生する
-
Coroutine
- プロセス上のメモリを利用するので、個別にメモリを確保する必要がない
- 切り替えタイミングをプログラマが指定するので、オーバーヘッドが発生しない
登場人物一覧
Coroutine利用時の登場人物について記載する
Coroutine : スレッドの代わりに利用できる軽量な非同期/ノンブロッキング処理のこと。Jobを継承している。
CoroutineContext : Coroutineを利用するための情報が全て入っている。ここでCoroutineが実行される
CoroutineDispatcher : Coroutineが実行されるスレッドを決定するもの。CoroutineContextに含まれている
Job : バックグラウンドで行う仕事。CoroutineBuilderにて作成されたコルーチンはJobとして扱われる。CoroutineContextに含まれている
CoroutineScope : コルーチンの有効期間を管理する。CoroutineContextを一つだけ持っている
CoroutineBuilder : Coroutineのビルダー。CoroutineScopeの拡張関数で定義されている
関係図
基本的な実装
AndroidではよくViewModelがCoroutineの起点となるので、ViewModelにCoroutineを実装した場合のソースコードを以下に記載する。
ViewModel内でCoroutineを作成する(基本例)
基本的には以下の手順を踏む。
- CoroutineScopeを継承する
- Jobを作成する
- coroutineContextにDispatchersとJobを足したものを保存しておく
- launchメソッドでコルーチンを作成する
- onClearedでJobをキャンセルしておく
以下のソースコードに各手順でのコメントを記載している。
/**
* ①CoroutineScopeを継承する
* CoroutineScopeを継承することで、Coroutineを作成することができるようになる。
* (coroutineBuilderを利用できるようになる)
*/
class TestViewModel(val todoRepository: TodoRepository) : ViewModel(), CoroutineScope {
/**
* ②Jobを作成する。
* ここでインスタンス化し、coroutineContextに加えることで
* このViewModelで実行している全てのJobをキャンセルできる。
* onCleared(viewModelが死ぬタイミングで呼ばれるメソッド)でjobのキャンセルを行う場合が多い
* (死んだ後はコルーチンに参照できなくなっちゃう)
*/
private val job = Job()
/**
* ③coroutineが実際に動作するcoroutineContextを作成する。
* Dispatchers.MainとJobは共にCroutineContextを継承しており、
* また、coroutineContextはplusオペレータをoverrideして実装しているため足すことができる
*/
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
fun add (task : Task) {
/**
* ④launchメソッドでCoroutineを作成する
* launchはCoroutineScopeの拡張関数として定義されているメソッドで、
* CoroutineScopeが持っているCoroutineContextを利用してCoroutineを作成する、CoroutineBuilderである。
* そのため、Dispatchers, Job 共にCoroutineScopeが持つものになる。
* なのでこのまま実行してもメインスレッドでの実行になってしまうため、contextにDispatchers.IOを設定する。
* これは現状のcoroutineContextに足されるため、JobはそのままにDispatchersだけ変更される
*/
launch(context = Dispatchers.IO){ todoRepository.add(task) }
}
/**
* ⑤onCleared()でjobをキャンセルする
* ViewModelが死ぬタイミングで全てのコルーチンはキャンセルしておく。
*/
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
押さえておきたいところ
CoroutineBuilderは他に何があるのか
基本的にはlaunchとasyncだけ押さえておけば良さそう。
- launch: 戻り値はJob(Coroutine自体)を。その戻り値を使ってJobの終了を待ったり、キャンセルしたりできる。
- async : 戻り値はDeferred。await()を利用してCoroutineが返す値を取得できる。
ただしawait()はsuspend functionのため、Coroutineの中(もしくはsuspend function)でないと利用できない。
Dispatchersは他に何があるのか
以下の4つがある
- Dispatchers.Main
- Main Threadを指定する
- Dispatchers.Default
- バックグラウンドスレッドの共有プールを利用する。スレッド最大数はCPUコアの数となる。(ただ、少なくとも2つはあるらしい)
- Dispatchers.IO
- オンデマンドで追加のスレッドが立つ。デフォルトでは64スレッドかCPUコア数のどちらか大きい方となる。(システムプロパティで数は変更できる)
- Dispatchers.Unconfined
- 特定のスレッドに限定されない。公式リファレンスに普通のコードでは使わないようにと太文字で記載されている。
どう使い分けるべきか(私見)
基本的に非同期で何らかの計算をするのであればDispatchers.Defaultを利用し、通信をする場合はDispatchers.IOを利用する形で良いと思ってます。
複数の通信が同時に走る(例えば、リストスクロールでリスト内の画像を取得する)場合、スマホの最大コア数までのスレッド数は心もとない。けどオンデマンドでスレッド立てるようならCoroutineのメリットが減少するのではないかなぁという気持ちです。
もちろんUI処理をする場合はDispatchers.Mainを利用します。
ViewModelのDispatchersは基本Mainでいいと思います。
Dispatchers.Unconfinedは「急ぎ何でも良いからスレッド借りて処理したい」という場合に使えそうですが、UIスレッド使われる場合もありそうなので、基本使わない方がいいんじゃないかなぁ。
withContext vs async / await
以下の二つの処理は「同じ処理Aの終了を待ち、処理Bを行う」挙動をする。
fun addAndFetchWithContext(task : Task) {
launch {
withContext(Dispatchers.Default) {
// 非同期処理A
}
// 処理B
}
}
fun addAndFetchAsync(task : Task) {
launch {
async {
// 処理A
}.await()
// 処理B
}
}
しかし、withContextが現在のコルーチンのDispatchersを切り替えるのに対し、asyncは新たにコルーチンを生成し、待機することになる。
コルーチン自体軽量なためパフォーマンスにそこまで影響があるわけではないが、基本的には「ある処理をコルーチン内で待ち、データを取得する」だけであればwithContextを使うべきである。
ではasync/awaitはどのタイミングで利用するべきなのか。これはRxのzip処理と同じように、複数の非同期処理を待つ場合に使えそう。
fun addAndFetchAsync(task : Task) {
launch {
val func1 = async { repositoryA.fetch() }
val func2 = async { repositoryB.fetch() }
data = Data(func1.await(), func2.await())
}
}
よく見るGlobalScopeは何なのか
GlobalScopeはその名の通りライフサイクルがアプリケーションのScopeとなる。
基本的には各ライフサイクルに沿ったCoroutineScopeを定義して利用すべきで、GlobalScopeはライフサイクルに依存しない処理を実装したい時(キャンセルされたくない処理)に利用する。
よく見るrunBlockingは何なのか
現在のスレッドから新しいコルーチンを作成し、そのコルーチンが終わるまで処理を中断しながらブロックする。これは通常のブロッキングコードを関数やテストで使用するためのメソッドで、コルーチン内で書いてはいけない。(その用途で設計していないそう。)
ViewModel内でCoroutineを作成する(viewModelScope)
lifecycle-viewmodel-ktx:2.1.0 (現状alpha-04)からviewModelScopeが利用できるようになった。
これはViewModelがすでにScopeを持っているため、上記①〜③の処理を自分で書かずともよくなっている。
class AddTodoViewModel(private val todoRepository: TodoRepository) : ViewModel() {
fun onClickSaveButton(title: String, detail: String, date: String) {
viewModelScope.launch {
val task = Task(title, detail, date)
todoRepository.add(task)
}
}
class Factory(private val todoRepository: TodoRepository) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AddTodoViewModel(todoRepository) as T
}
}
@ExperimentalCoroutinesApi
override fun onCleared() {
super.onCleared()
viewModelScope.cancel()
}
}
備考
これを書いてたらGoogleI/O 2019でLiveData + Coroutine の話が出てきたのでこれから追っかけます。
(2019/05/09追記)
書いたのでリンク貼っておきます。
https://qiita.com/offwhite/items/5d7e71ad541b7a222f36
参照
- withContext
- GlobalScope
- runBlocking