Coroutine:runBlockingビルダーでコルーチン開始

投稿日:  更新日:

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

Kotlinが提供します。

コルーチンはビルダー(Builder)により開始されます。

ビルダーは3つの種類があり、その中の1つがrunBlockingです。

このrunBlockingビルダーについて、まとめます。

スポンサーリンク

ビルダーとは

ビルダー(Builder)は、CoroutineScopeを起点にスレッドを起動して、引数で指定されたタスクブロックを実行します。

つまり、ビルダーの役割はコルーチンを開始することです。

        var scope: CoroutineScope? = null
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            scope = SampleScope()
            scope?.launch() {	                 // launchビルダー
                /* --- ここから ---              */
				/* 重い処理(長時間の処理)      */
				/* launch { ... }                */
				/*        ^^^^^^^ タスクブロック */
                println("Thread = ${Thread.currentThread().name}")
				/* --- ここまで ---              */
            }
        }
        findViewById<Button>(R.id.btnCancel).setOnClickListener {
            scope?.cancel()
        }
class SampleScope : CoroutineScope {
    override val coroutineContext: CoroutineContext =
        Job() + Dispatchers.Default + CoroutineName("Hoge") // Contextを定義
}

ただし、runBlockingは他のビルダーど異なり、CoroutineScopeを起点にしません。この点については後述します。

        findViewById<Button>(R.id.btnStart).setOnClickListener {
            runBlocking() {	                     // runBlockingビルダー
                /* --- ここから ---              */
                /* 重い処理(長時間の処理)      */
                /* launch { ... }                */
                /*        ^^^^^^^ タスクブロック */
                println("Thread = ${Thread.currentThread().name}")
                /* --- ここまで ---              */
            }
        }
スポンサーリンク

runBlockingの特徴

ビルダーは3つの種類があります。

その中のrunBlockingは自身を実行したスレッドをブロックするビルダーです。

ビルダー概要関数の戻り値
launchスレッドを起動してタスクブロックを実行
タスクブロックの戻り値なし
スレッドをブロックしない(一時停止)
起動したスレッドのJobインスタンス
asyncスレッドを起動してタスクブロックを実行
タスクブロックの戻り値あり
スレッドをブロックしない(一時停止)
起動したスレッドのDeferredインスタンス
※DeferredはJobのサブクラス
runBlockingスレッドを起動してタスクブロックを実行
タスクブロックの戻り値あり
スレッドをブロックする(休止)
タスクブロックの戻り値

自身を実行したスレッドをブロックしてしまったら、それは非同期処理になりません。

非同期処理を目的としたコルーチンの意義が失われてしまいます。

ですので、runBlockingがアプリのプログラム中に登場することは無いと思われます。用途はテストやサンプルなどでしょう。

また、runBlockingは次のように定義されています。

public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext, 
	block: suspend CoroutineScope.() -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

runBlockingはCoroutineScopeの拡張関数ではありません。従って、起点のCoroutineScopeを必要としません。

これは、GlobalScopeというCoroutineScopeをrunBlockingの定義内に持っているからです。runBlockingはGlobalScopeを参照します。

GlobalScopeはライブラリで提供されている既成のCoroutineScopeです。シングルトンなので必ず同じインスタンスを返します。

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}
スポンサーリンク

スレッドの起動(第2引数)

スレッドの起動はrunBlockingの引数(第2引数)にラムダ式(タスクブロック)を指定して実行するだけです。※引数の最後がラムダ式の場合、「( )」の外に出せる

この場合、起動されるスレッドはrunBlockingを実行したスレッドになります。GlobalScopeが持つCoroutineContextがEmptyCoroutineContext(全ての属性の定義が空)であるためです。

            // runBlockingを実行したスレッドと同じスレッドを起動
            runBlocking {
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _resultA = taskA()
                println("Time = ${getTime()}   ResultA = ${_resultA}")

                println("Time = ${getTime()} End")
            }
            println("Time = ${getTime()} After Coroutine!")
    fun taskA(): String {
        Thread.sleep(1000)     // 重い処理の代わり
        return "AAA"           // 重い処理の戻り値
    }
    fun taskB(): String {
        Thread.sleep(2000)     // 重い処理の代わり
        return "BBB"           // 重い処理の戻り値
    }
    fun taskC(): String {
        Thread.sleep(3000)     // 重い処理の代わり
        return "CCC"           // 重い処理の戻り値
    }
getThread, getTime
    inline private fun getTime(): String {
        return "%04d".format(System.currentTimeMillis() % 10000)
    }
    inline private fun getThread(): String {
        return Thread.currentThread().name
    }

runBlockingでスレッドの起動

Time = 4551 Start Thread = main
Time = 5558   ResultA = AAA
Time = 5558 End
Time = 5558 After Coroutine!
Skipped 60 frames!  The application may be doing too much work on its main thread.
スポンサーリンク

スレッドの切り替え(第1引数)

runBlockingビルダーの第1引数はCoroutineContextです。。

ここに新たなCoroutineContextを指定すると、引数で指定されたCoroutineContextが使用されます。

次の例は、Dispatchers.Defaultを指定することで、スレッドプールをMainからDefaultに切り替える例です。runBlockingによりDefaultスレッドが起動されます。

※CoroutineContextの詳細は「Kotlin:コルーチン(Coroutine)」を参照

            // ビルダーの引数でプールを指定、Defaultスレッドで起動
            runBlocking(Dispatchers.Default) {
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _resultA = taskA()
                println("Time = ${getTime()}   ResultA = ${_resultA}")

                println("Time = ${getTime()} End")
            }
            println("Time = ${getTime()} After Coroutine!")

runBlockingでスレッドの切り替え

Time = 3702 Start Thread = DefaultDispatcher-worker-1
Time = 4704   ResultA = AAA
Time = 4704 End
Time = 4705 After Coroutine!
Skipped 60 frames!  The application may be doing too much work on its main thread.

runBlockingを実行したMainスレッドがブロックされる点に注意してください。

「Skipped 60 frames! …」はMainスレッドがブロックされたことで、画面描画(リフレッシュ)が行えなかったことを知らせるメッセージです。

※Androidはフレームレート:60[fps]を目標に動作しています。これは1秒間に60回の画面描画を行うことです。

スポンサーリンク

タスク戻り値の取得

runBlockingビルダーはタスクブロックの戻り値をそのまま返します。

            // タスクの戻り値を取得
            val _result = runBlocking(Dispatchers.Default) {
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _resultA = taskA()

                println("Time = ${getTime()} End")
                _resultA    // 戻り値:最後に評価
            }
            println("Time = ${getTime()} Result = ${_result}")

ラムダ式の戻り値はタスクブロック内で最後に評価した値になります。ですので、この例はtaskA( )を実行した結果が戻り値です。

タスク戻り値の取得

Time = 2156 Start Thread = DefaultDispatcher-worker-2
Time = 3159 End
Time = 3160 Result = AAA
Skipped 60 frames!  The application may be doing too much work on its main thread.
スポンサーリンク

ブロックの期間

runBlockingは自身を実行したスレッドをブロックします。

そのブロックする期間は「コルーチンから実行されたすべての処理が終了するまで」です。

以下の例はrunBlockingで起動されたスレッド(親)から、さらに子・孫のスレッドを起動する例です。

親と子スレッドは一瞬で処理が終わりますが、孫スレッドは重い処理を含んでいます。

            // スレッドがブロックされる期間
            val _result = runBlocking(Dispatchers.Default) {
                println("Time = ${getTime()} Start Thread = ${getThread()}")
                launch {
                    println("Time = ${getTime()}   Start Thread = ${getThread()}")
                    launch { Thread.sleep(1000) }
                    println("Time = ${getTime()}   End   Thread = ${getThread()}")
                }
                println("Time = ${getTime()} End   Thread = ${getThread()}")
                "AAA"   // // 戻り値:最後に評価
            }
            println("Time = ${getTime()} Result = ${_result}")

スレッドがブロックされる期間は孫スレッドが終了するまでになります。

スレッドがブロックされる期間

Time = 4178 Start Thread = DefaultDispatcher-worker-2
Time = 4196 End   Thread = DefaultDispatcher-worker-2
Time = 4196   Start Thread = DefaultDispatcher-worker-1
Time = 4199   End   Thread = DefaultDispatcher-worker-1
Time = 5203 Result = AAA
Skipped 62 frames!  The application may be doing too much work on its main thread.
スポンサーリンク

関連記事:

近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 特徴としてnon-blocking動作をサポートします。 このnon-blocking動作についてまとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンの構成要素であるSuspend関数について、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがlaunchです。 このlaunchビルダーについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するlaunchビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがasyncです。 このasyncビルダーについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するasyncビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
CoroutineContextはコルーチンで起動されるスレッドの属性を格納しています。 その中にコルーチンの名前を表現するName属性があります。 Name属性を出力する方法を紹介します。 ...
コルーチン(Coroutine)は「非同期処理プログラミングの手法」の1つです。 Kotlinが提供します。 withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。 このwithContextについて、まとめます。 ...
コルーチン間でメッセージ(データのこと)の送受信を行うことが出来ます。 これにより、処理の投げっぱなしになりがちな非同期処理と、連携を強化できます。 しかも、ProduceやFlowを使うと記述が簡素になり、プログラミングの容易さと読みやすさが向上して便利です。 今回は、この「メッセージの送受信」について、使い方をまとめました。 ※環境:Android Studio Flamingo | 2022.2.1    :org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 ...
スポンサーリンク