コルーチンの基本
本連載は、Kotlinのバージョン1.1から2.0までのアップデート内容を、テーマごとにバージョン横断で紹介する連載です。その連載も、今回でいったんの区切りとなります。今回のテーマは、コルーチンです。
非同期処理とコルーチン
コルーチンは、Kotlinで非同期処理を実現するための機能です。Android開発においては、多用される仕組みですが、実は、Kotlin言語のバージョン1.0の正式リリース時には存在せず(より正確には実験的機能であり)、後発のバージョン1.3で正式導入されたものです。
図1は、main()関数から、longProcess()関数とshow()関数を呼び出している処理の流れを図式化したものです。そして、それぞれの関数を呼び出している間、main()関数の処理は待ち状態となります。他の関数から、実行処理がmain()に戻ると同時に、main()関数内の続きの処理が再開します。
つまり、関数の戻りと処理の再開が同期することから、この仕組みを同期処理と言い、Kotlinの処理体系はその基本が同期処理となります。
ここで、longProcess()関数内の処理に時間がかかるものとします。すると、main()関数内の処理は続きの処理ができなくなります。つまり、処理がブロックされます。
これを避けるために、図2のように、main()関数はlongProcess()関数の処理の戻りを待たずに続きの処理を行うようにします。その場合、関数からの処理の戻りと処理の再開が同期しません。
この仕組みを、非同期処理と言います。あるいは、main()関数内の処理がブロックされないことから、ノンブロッキング処理とも言います。
Kotlin言語の元となる(JVM言語の主と言える)Java言語では、この非同期処理を実現するためにスレッドの分離という方法を採用せざるを得ません。もちろん、同じJVM上で動作するKotlinでは、同じくスレッドの分離という方法も可能ですが、同一スレッドで動作した上で非同期処理を実現できる仕組みとして、コルーチンが導入されています。
コルーチンとは処理を中断できる仕組み
コルーチンの仕組みを、先の図1で表すと図3の通りです。
図3の緑の部分のように、longProcess()の呼び出しから戻りまでの一連の処理をコルーチンとして定義します。すると、Kotlinのランタイム(実行環境)は、もしコルーチン内の処理に時間がかかるようならば、いつでもその処理を中断し、コルーチン外の続きの処理を先に行えるようになっています。その後、コルーチン内の処理の完了を待ちます。これにより、非同期処理が実現できるようになっています。
コルーチンの定義はlaunchメソッド
コルーチンを定義する構文は、以下の通りです。CoroutineScopeインスタンスのlaunch()メソッドを実行し、その引数ラムダ式内にコルーチンとして定義したい処理、つまり、非同期で行いたい処理を記述します。
コルーチンスコープ.launch {
コルーチンとして定義したい処理
}
ということは、まず、コルーチンスコープ(CoroutineScopeインスタンス)を用意する必要があります。これが、Android開発ならば、SDK内に用意されており、アクティビティやフラグメントなどのライフサイクル対応コンポーネント内で利用できるlifecycleScopeと、ViewModel内で利用できるviewModelScopeがあります。
例えば、lifecycleScopeで図3の処理を実現したコード例を挙げると、リスト1のようになります。
override fun onCreate(savedInstanceState: Bundle?) {
:
lifecycleScope.launch {
longProcess()
}
show()
}
ピュアKotlinでのコルーチン定義
一方、ピュアなKotlinでコルーチンを定義する場合は、表1の2種類の方法があります。
| 方法 | 利用できる場所 |
|---|---|
| runBlocking関数内 | main()関数内 |
| coroutineScope関数内 | suspend関数内 |
このうち、まずrunBlocking()関数の使い方を紹介します。
例えば、リスト2のコードの通りです。なお、リスト2中で利用しているprintMsgWithTime()関数は、引数のメッセージをその時点での時刻とスレッド番号とともに標準出力する関数です。
fun main() {
runBlocking { // (1)
printMsgWithTime("main開始") // (2)
launch { // (3)
printMsgWithTime("コルーチン開始") // (4)
val startTime = LocalTime.now()
while(true) {
val now = LocalTime.now()
val duration = Duration.between(startTime, now)
val diff = duration.toMillis()
if(diff > 1000) {
break
}
}
printMsgWithTime("コルーチン終了") // (5)
}
printMsgWithTime("main終了") // (6)
}
}
リスト2の(3)でlaunch()メソッドを実行しています。すると、その引数ラムダ式内の処理、すなわち、launch以降の{ }ブロック内の処理がコルーチンとなります。
ただし、そのlaunch()メソッドの前にはCoroutineScopeインスタンスを表す変数が記述されていません。代わりに記述されているのが(1)のrunBlocking()関数の実行です。main()関数内でrunBlocking()関数を実行すると、その引数ラムダ式、すなわち、(2)~(6)までのコード内では暗黙的インスタンスとしてCoroutineScopeが存在することになります。もしこれを変数で表すとするなら、thisです。つまり(3)のコードは、正確に記述するならば、this.launchとなります。ただし、このthisは通常記述しませんので、単にlaunchと記述します。
これが、表1のひとつめのlaunch()メソッドの実行方法です。
コルーチンが非同期処理になることの確認
ここで、リスト2の実行結果を確認しながら、処理内容を見ていきましょう。
リスト2の実行結果にはその時点での時刻とスレッド番号が含まれているので、その都度違います。例えば、リスト3の通りになります。各行の末尾の3という数字がスレッド番号です。なお、実行結果中のコメントの番号は、リスト2のコードに対応しています。
main開始: 02:50:51.528402; 3 // (2) main終了: 02:50:51.532604; 3 // (6) コルーチン開始: 02:50:51.535779; 3 // (4) コルーチン終了: 02:50:52.536883; 3 // (5)
リスト2のコルーチン内では、無限ループを実行し、1秒(1000ミリ秒)経過するとループを抜けるようになっています。このことから、リスト2の(4)と(5)の間には1秒間の経過が発生します。実際に、実行結果(リスト3)の(4)と(5)を見ると、その通りになっています。
ここで注目すべきなのは、リスト2の(6)の実行順序です。1秒というコンピュータからすると長い時間かかる処理というのをコルーチンにすることによって、その処理を後回しにし、先にコルーチン外の(6)の処理を行っているのが実行結果からわかります。
これにより、図3の通り、コルーチンを利用することで非同期処理が実現できていることがわかります。
