コルーチン(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" // 重い処理の戻り値 }
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!")
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.
関連記事: