コルーチン(Coroutine)は「非同期処理プログラミングの手法」の1つです。
Kotlinが提供します。
withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。
このwithContextについて、まとめます。
withContextとは
withContextは、引数に指定されたCoroutineContextへ切り替えて、タスクブロックを実行するSuspend関数です。
また、タスクの戻り値を返すことができます。
内部は次のように記述されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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の最も有効な場面はスレッドを切り替えたい時です。
1 2 3 4 5 6 7 8 9 10 11 12 | // 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" ) } |
1 2 3 4 | 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は子孫(入れ子の子・孫…)に引き継がれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 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で同様に行うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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になる場合です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 同一な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になる場合です。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 同値な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のスレッド名が同じです。つまり、スレッドの切り替えは行われていません。
関連記事: