6
6

More than 1 year has passed since last update.

Kotlin コルーチンの標準ライブラリのみでコルーチンを使ってみた

Last updated at Posted at 2022-09-12

はじめに

Kotlin コルーチンは Web 上の様々なドキュメントや、本家が提供する Coroutines guide 等を読んでいけば割と簡単に使うことができます。例えば Android アプリを作成する時にドキュメントやサンプルコードを読むと Kotlin コルーチンを使う例がたくさん出てきますので、それを真似しながら上記の本家のガイド等で調べて使いこなしていくという感じになると思います。

一方コルーチンをある程度使っていくと、コルーチンの豊富な機能はどのように作れらているのか興味が湧いてきます(きません?)。Kotlin コルーチンは以下の2つのライブラリから作られており、標準ライブラリが提供する基本的な機能の上に高水準なライブラリが作られ様々な機能を提供しています。

  • kotlin.coroutines
    : 基本的で限定された機能を提供する標準ライブラリ
  • kotlinx.coroutines
    : 標準ライブラリを利用して豊富な機能を提供する高水準なライブラリ

ここではコルーチンを理解するための一つの方法として標準ライブラリのみを使って何ができるのかを色々探っていきたいと思います。

意外なことにコルーチンを使い始めればすぐに出会う CoroutineScope は標準ライブラリではなく高水準ライブラリの方に含まれています。そうすると標準ライブラリのみを使うという制約では CoroutineScope.launchCoroutineScope.async は使えないことになります。じゃあどうやってコルーチンを作って実行するんだろう?というのを実験をしながら調べていこうということです。

実験環境

ここでは Android Studio を使って Android アプリを作成し、その上で色々実験していこうと思います。

Android Studio を立ち上げ新しいプロジェクトを「Empty Activity」を雛形にして作成し、何も変更せずにプロジェクトを作成します。MainActivy.kt ファイルが作成されるので、その上に実験用のコードを埋め込みログ出力を使うことで動作を確認していきます。

コルーチンを使うために app/build.gradle に以下のライブラリ依存関係を追加しましょう。(2022-9-22 追記)

dependencies {
    // ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}

コルーチンの作成と実行

コルーチンの作成は標準ライブラリに含まれる createCoroutine 関数で行います。

createCoroutine は拡張関数で、引数無しの suspend 関数をレシーバーとして呼び出すことでその関数を実行するためのコルーチンを作成します。作成時にはコルーチンは停止しています。

fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit>

例えば

import kotlinx.coroutines.delay

private suspend fun mySuspendFunc(): String {
    repeat(5) {
        delay(1_000) // 実は高水準ライブラリの関数
        Log.d("MyLog", "mySuspendFunc: index $it")
    }
    return "exit from mySuspendFunc"
}

の様な suspend 関数に対し

import kotlin.coroutines.*

val cont = ::mySuspendFunc.createCoroutine(
    object : Continuation<String> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<String>) {
            Log.d("MyLog", "result: ${result.getOrNull()}")
        }
    })

の様に使うことで mySuspendFunc 関数を実行するためのコルーチンを作成します。

createCoroutine 関数は Continuation オブジェクトを返します。このオブジェクトに対し resume(Unit) 関数を呼ぶことでコルーチンを開始することができます。

cont.resume(Unit)

Continuation は以下の様に定義されています。

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

@InlineOnly
public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

// その他は省略

createCoroutine 関数の引数は Continuation オブジェクトです。suspend 関数が終了するとその戻り値は Result オブジェクトにラップされこのオブジェクトの resumeWith 関数に渡されます。

また context プロパティは Continuation に関するCoroutineContext です。これがどういう役割をするのかドキュメントからはちょっとよく分からなかったのですが、恐らくコルーチンを再開する時の実行環境を設定するのでしょう。CoroutineContext によく渡される JobDispatchers.Default は高水準ライブラリで定義されているためここでは使用できません。

標準ライブラリでは EmptyCoroutineContext のみが定義されているのでこれを使います。つまり createCoroutine 関数に渡す Continuation オブジェクトの実行環境は特になにも指定しないことになります。

それでは実験環境として作成した MainActivity クラスで動作を確認してみます。テストは簡単のため onCreate 関数上で行います。

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.delay
import kotlin.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val cont = ::mySuspendFunc.createCoroutine(
            object : Continuation<String> {
                override val context: CoroutineContext = EmptyCoroutineContext

                override fun resumeWith(result: Result<String>) {
                    Log.d("MyLog", "result: ${result.getOrNull()}")
                }
            })

        cont.resume(Unit)
    }
}

private suspend fun mySuspendFunc(): String {
    repeat(5) {
        delay(1_000) // 実は高水準ライブラリの関数
        Log.d("MyLog", "mySuspendFunc: index $it")
    }
    return "exit from mySuspendFunc"
}

実機なりエミューレーター上で実行し Android Studio の Logcat 画面で確認すると

17:05:09.862 D/MyLog: mySuspendFunc: index 0
17:05:10.863 D/MyLog: mySuspendFunc: index 1
17:05:11.864 D/MyLog: mySuspendFunc: index 2
17:05:12.866 D/MyLog: mySuspendFunc: index 3
17:05:13.867 D/MyLog: mySuspendFunc: index 4
17:05:13.867 D/MyLog: result: exit from mySuspendFunc

とちゃんと1秒おきにログを出力しています。

さて createCoroutine 関数を直に呼んでいるとソースコードが見にくくなりますし、 launch や async の様に実行したい suspend 関数を引数として渡せると便利ですのでその様な関数を作成してみます。

import kotlin.coroutines.*

private fun <T> callSuspendFunc(block: suspend () -> T) {
    block.createCoroutine(object : Continuation<T> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<T>) {
            Log.d("MyLog", "result: ${result.getOrNull()}")
        }
    }).resume(Unit)
}

これで

callSuspendFunc {
    // コルーチンで実行
}

の様に引数無しのラムダ式を渡すことでコルーチンを作成しラムダ式を実行することができます。何となく launch 関数のようになりました。

ちなみに作成と同時に実行を開始する場合は startCoroutine 関数を使うこともできます。

private fun <T> callSuspendFunc2(block: suspend () -> T) {
    block.startCoroutine(object : Continuation<T> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<T>) {
            Log.d("MyLog", "result: ${result.getOrNull()}")
        }
    })
}

ではこれも動作確認をしてみます。

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.delay
import kotlin.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        callSuspendFunc {
            repeat(5) {
                delay(1_000) // 実は高水準ライブラリの関数
                Log.d("MyLog", "mySuspendFunc: index $it")
            }
            "exit from callSuspendFunc"
        }
    }
}

private fun <T> callSuspendFunc(block: suspend () -> T) {
    block.createCoroutine(object : Continuation<T> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<T>) {
            Log.d("MyLog", "result: ${result.getOrNull()}")
        }
    }).resume(Unit)
}

ログを確認すると

17:27:15.362 D/MyLog: mySuspendFunc: index 0
17:27:16.364 D/MyLog: mySuspendFunc: index 1
17:27:17.365 D/MyLog: mySuspendFunc: index 2
17:27:18.367 D/MyLog: mySuspendFunc: index 3
17:27:19.368 D/MyLog: mySuspendFunc: index 4
17:27:19.368 D/MyLog: result: exit from callSuspendFunc

となり、期待通り動作していることが確認できます。

さてソースコードのコメントにも記述してあるように、実はここで使用している delay 関数は高水準ライブラリに含まれており、標準ライブラリのみ使うという原則に違反しています。ここでは簡単のためあえてこれを使いました。次はこの delay 関数を標準ライブラリのみで実装してみます。

独自の delay 関数を実装する。

delay 関数の様な機能を実装するためには標準ライブラリに含まれる suspendCoroutine 関数を使います。

suspend inline fun <T> suspendCoroutine(
    crossinline block: (Continuation<T>) -> Unit
): T

この関数はコルーチンを再開させるための Continuation オブジェクトを block 関数に渡し、現在実行中のコルーチンをサスペンドします。 block 関数に渡された Continuation オブジェクトに対しresume 関数を呼び出すことにより停止しているコルーチンが再開します。

delay 関数の様に一定時間動作を中断するために Android の Handler オブジェクトと Hander.postDelayed 関数を使用します。

fun postDelayed(
    r: Runnable,
    delayMillis: Long
): Boolean

この関数は delayMillis ミリ秒後に実行するようにメッセージキューに r 関数を加えます。これと suspendCoroutine 関数を組み合わせて

private val handler = Handler(Looper.getMainLooper())

suspend fun myDelay(timeMillis: Long) {
    return suspendCoroutine { cont: Continuation<Unit> ->
        val runnable = Runnable {
            cont.resume(Unit)
        }
        handler.postDelayed(runnable, timeMillis)
    }
}

の様にすれば標準ライブラリのみで delay 関数のような機能を実現できます。

実験環境で動かしてみます。

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlin.coroutines.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        callSuspendFunc {
            repeat(5) {
                myDelay(1_000) // 自作のdelay関数
                Log.d("MyLog", "mySuspendFunc: index $it")
            }
            "exit from callSuspendFunc"
        }
    }
}

private fun <T> callSuspendFunc(block: suspend () -> T) {
    block.createCoroutine(object : Continuation<T> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<T>) {
            Log.d("MyLog", "result: ${result.getOrNull()}")
        }
    }).resume(Unit)
}

private val handler = Handler(Looper.getMainLooper())

suspend fun myDelay(timeMillis: Long) {
    return suspendCoroutine { cont: Continuation<Unit> ->
        val runnable = Runnable {
            Log.d("MyLog", "resume continuation")
            cont.resume(Unit)
        }
        handler.postDelayed(runnable, timeMillis)
    }
}

結果は

19:17:05.250 D/MyLog: resume continuation
19:17:05.250 D/MyLog: mySuspendFunc: index 0
19:17:06.253 D/MyLog: resume continuation
19:17:06.253 D/MyLog: mySuspendFunc: index 1
19:17:07.255 D/MyLog: resume continuation
19:17:07.256 D/MyLog: mySuspendFunc: index 2
19:17:08.258 D/MyLog: resume continuation
19:17:08.258 D/MyLog: mySuspendFunc: index 3
19:17:09.260 D/MyLog: resume continuation
19:17:09.260 D/MyLog: mySuspendFunc: index 4
19:17:09.261 D/MyLog: result: exit from callSuspendFunc

となり、ちゃんと1秒おきにログを出力しています。

まとめ

Kotlin コルーチンの標準ライブラリのみを使用して、コルーチンを作成し動作させることができました。原始的な launch 関数のような物も作成でき、動作させることができました。また delay 関数の様な suspend 関数も標準ライブラリのみを使用して作成することができました。

ちなみに今回の件を調べるために標準ライブラリのソースコードを追ったりしたのですが、実装が見つからず

/**
 * Returns the context of the current coroutine.
 */
@SinceKotlin("1.3")
@Suppress("WRONG_MODIFIER_TARGET")
@InlineOnly
public suspend inline val coroutineContext: CoroutineContext
get() {
    throw NotImplementedError("Implemented as intrinsic")
}

のようにエラーを投げるコードしか見つからないことがありました。
このエラーメッセージ "Implemented as intrinsic" は要はこの機能はコンパイラが実装する機能で、
kotlin のソースコードでは実装していないという意味らしいです。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6