コルーチン(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のスレッド名が同じです。つまり、スレッドの切り替えは行われていません。
関連記事:
