コルーチン(Coroutine)は「非同期処理プログラミングの手法」の1つです。
Kotlinが提供します。
withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。
このwithContextについて、まとめます。
withContextとは
withContextは、引数に指定されたCoroutineContextへ切り替えて、タスクブロックを実行するSuspend関数です。
また、タスクの戻り値を返すことができます。
内部は次のように記述されています。
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を定義 }
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の動作(仕組み)はほぼ同等ですが、比較すると次にあげる違いがあります。
項目 | withContext | async |
---|---|---|
記述 | val result = withContext(context) { タスク } | val result = async(context) { タスク }.await() |
コルーチンの機能名 | Suspend関数 | ビルダー |
実行の所在 | Suspend関数内で実行 | CoroutineScopeを起点に起動 |
動作(仕組み) | ほぼ同等 ・タスクブロックの実行方法 ・結果の返し方 |
|
スタートオプション | なし | あり |
スレッド切り替え | 以下の条件で切り替えない ・同一なContext ・同値なDispatchers ※カレントのスレッドで処理 | 必ず切り替える ※切り替えの前後で 同じスレッドになる場合がある |
※context:CoroutineContext |
スレッドを切り替えない条件
「asyncとの比較」でも書きましたが、withContextはスレッドを切り替えない条件があります。
この時、タスクブロックは現在のスレッドで処理されます。
同一な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
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
topとsubのスレッド名が同じです。つまり、スレッドの切り替えは行われていません。
関連記事: