Coroutine:withContextでスレッドを切り替え

投稿日:  更新日:

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

Kotlinが提供します。

withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。

このwithContextについて、まとめます。

スポンサーリンク

withContextとは

withContextは、引数に指定されたCoroutineContextへ切り替えて、タスクブロックを実行するSuspend関数です。

また、タスクの戻り値を返すことができます。

Contextの切り替え

内部は次のように記述されています。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        val oldContext = uCont.context
        val newContext = oldContext + context  // newContextを基にスレッドを起動
        newContext.checkCompletion()
		// 同一なContextの場合
        if (newContext === oldContext) {
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
		// 同値なDispatchersの場合
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
		// 異なるContextの場合
        val coroutine = DispatchedCoroutine(newContext, uCont) // コールバックを受け取る
        coroutine.initParentJob()
        block.startCoroutineCancellable(coroutine, coroutine)  // 新スレッドでタスク実行
        coroutine.getResult()   // COROUTINE_SUSPENDEDを返す⇒旧スレッドは一時停止
    }
}

「CoroutineContextの切り替え」は、現状のContextと引数のContextの加算で生まれたnewContextを基に、スレッドを起動しているだけです。

スポンサーリンク

スレッドを切り替え

withContextの最も有効な場面はスレッドを切り替えたい時です。

            // withContextでスレッドの切り替え
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _result = withContext(Dispatchers.Default) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
class SampleScope : CoroutineScope {
    override val coroutineContext: CoroutineContext =
        Job() + Dispatchers.Default + CoroutineName("Hoge") // Contextを定義
}

withContextでスレッドの切り替え

Time = 7439 Start top Thread = main
Time = 7441   Start sub Thread = DefaultDispatcher-worker-1
Time = 8444 Result = ***
Time = 8444 End   top

withContextで切り替えられたCoroutineContextは子孫(入れ子の子・孫…)に引き継がれます。

            // withContextの子供
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _result = withContext(Dispatchers.Default) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    async {                  // withContextのCoroutineContextが引き継がれる
                        println("Time = ${getTime()}     Start sub*2 Thread = ${getThread()}")
                        Thread.sleep(1000)     // 重い処理の代わり
                        "***"                  // 重い処理の戻り値
                    }.await()
                }
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
Time = 2044 Start top Thread = main
Time = 2054   Start sub Thread = DefaultDispatcher-worker-2
Time = 2057     Start sub*2 Thread = DefaultDispatcher-worker-1
Time = 3062 Result = ***
Time = 3062 End   top
スポンサーリンク

asyncとの比較

「スレッドの切り替え」はasyncで同様に行うことができます。

        findViewById<Button>(R.id.btnStart).setOnClickListener {
            // asyncでスレッドの切り替え
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _result = async(Dispatchers.Default) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }.await()
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
        }
Time = 8179 Start top Thread = main
Time = 8190   Start sub Thread = DefaultDispatcher-worker-1
Time = 9193 Result = ***
Time = 9193 End   top

withContextとasyncの動作(仕組み)はほぼ同等ですが、比較すると次にあげる違いがあります。

項目withContextasync
記述val result = withContext(context) {
  タスク
}
val result = async(context) {
  タスク
}.await()
コルーチンの機能名Suspend関数ビルダー
実行の所在Suspend関数内で実行CoroutineScopeを起点に起動
動作(仕組み)ほぼ同等
・タスクブロックの実行方法
・結果の返し方
スタートオプションなしあり
スレッド切り替え以下の条件で切り替えない
 ・同一なContext
 ・同値なDispatchers
※カレントのスレッドで処理
必ず切り替える

※切り替えの前後で
 同じスレッドになる場合がある
※context:CoroutineContext
スポンサーリンク

スレッドを切り替えない条件

「asyncとの比較」でも書きましたが、withContextはスレッドを切り替えない条件があります。

この時、タスクブロックは現在のスレッドで処理されます。

Contextが同値・同一の場合

同一なContext

同一なContextになる場合です。

            // 同一なCoroutineContext
            scope = SampleScope()
            scope?.launch(Dispatchers.Default) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _result = withContext(EmptyCoroutineContext) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }
                Thread.sleep(100)          // subの開始よりも先にtopが終了するのを防ぐ
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
        }
Time = 6723 Start top Thread = DefaultDispatcher-worker-1
Time = 6726   Start sub Thread = DefaultDispatcher-worker-1
Time = 7729 Result = ***
Time = 7729 End   top
参考:同一なCoroutineContext(asyncの場合)
            // 同一なCoroutineContext(async)
            scope = SampleScope()
            scope?.launch(Dispatchers.Default) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _deferred = async(EmptyCoroutineContext) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }
                Thread.sleep(100)          // subの開始よりも先にtopが終了するのを防ぐ
                val _result = _deferred.await()
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
Time = 3926 Start top Thread = DefaultDispatcher-worker-1
Time = 3929   Start sub Thread = DefaultDispatcher-worker-2
Time = 4933 Result = ***
Time = 4934 End   top

topとsubのスレッド名が同じです。つまり、スレッドの切り替えは行われていません。

内部では以下のようなCoroutineContext加算が行われることになります。

   newContext = oldContext + EmptyCoroutineContext
   ※EmptyCoroutineContext:空のContext

この時、newContextとoldContextは同一(演算子:===)です。

同値なDispatchers

同値なDispatcherになる場合です。

            // 同値なDispatchers
            scope = SampleScope()
            scope?.launch(Dispatchers.Default) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _result = withContext(Dispatchers.Default) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }
                Thread.sleep(100)          // subの開始よりも先にtopが終了するのを防ぐ
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
Time = 8805 Start top Thread = DefaultDispatcher-worker-1
Time = 8807   Start sub Thread = DefaultDispatcher-worker-1
Time = 9810 Result = ***
Time = 9810 End   top
参考:同値なDispatchers(asyncの場合)
            // 同値なDispatchers
            scope = SampleScope()
            scope?.launch(Dispatchers.Default) {
                println("Time = ${getTime()} Start top Thread = ${getThread()}")
                val _deferred = async(Dispatchers.Default) {
                    println("Time = ${getTime()}   Start sub Thread = ${getThread()}")
                    Thread.sleep(1000)     // 重い処理の代わり
                    "***"                  // 重い処理の戻り値
                }
                Thread.sleep(100)          // subの開始よりも先にtopが終了するのを防ぐ
                val _result = _deferred.await()
                println("Time = ${getTime()} Result = ${_result}")
                println("Time = ${getTime()} End   top")
            }
Time = 4652 Start top Thread = DefaultDispatcher-worker-2
Time = 4653   Start sub Thread = DefaultDispatcher-worker-1
Time = 5657 Result = ***
Time = 5657 End   top

topとsubのスレッド名が同じです。つまり、スレッドの切り替えは行われていません。

スポンサーリンク

関連記事:

近頃の携帯端末はクワッドコア(プロセッサが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 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがrunBlockingです。 このrunBlockingビルダーについて、まとめます。 ...
CoroutineContextはコルーチンで起動されるスレッドの属性を格納しています。 その中にコルーチンの名前を表現するName属性があります。 Name属性を出力する方法を紹介します。 ...
コルーチン間でメッセージ(データのこと)の送受信を行うことが出来ます。 これにより、処理の投げっぱなしになりがちな非同期処理と、連携を強化できます。 しかも、ProduceやFlowを使うと記述が簡素になり、プログラミングの容易さと読みやすさが向上して便利です。 今回は、この「メッセージの送受信」について、使い方をまとめました。 ※環境:Android Studio Flamingo | 2022.2.1    :org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 ...
スポンサーリンク