コルーチン(Coroutine)は「非同期処理の手法」の1つです。
Kotlinが提供します。
コルーチンはビルダー(Builder)により開始されます。
ビルダーは3つの種類があり、その中の1つがasyncです。
このasyncビルダーについて、まとめます。
目次
ビルダーとは
ビルダー(Builder)は、CoroutineScopeを起点にスレッドを起動して、引数で指定されたタスクブロックを実行します。
つまり、ビルダーの役割はコルーチンを開始することです。
var scope: CoroutineScope? = null findViewById<Button>(R.id.btnStart).setOnClickListener { scope = SampleScope() scope?.async() { // asyncビルダー /* --- ここから --- */ /* 重い処理(長時間の処理) */ /* async { ... } */ /* ^^^^^^^ タスクブロック */ 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を定義 }
asyncの特徴
ビルダーは3つの種類があります。
その中のasyncはタスクブロックの戻り値を返すビルダーです。
ビルダー | 概要 | 関数の戻り値 |
---|---|---|
launch | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値なし スレッドをブロックしない(一時停止) | 起動したスレッドのJobインスタンス |
async | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値あり スレッドをブロックしない(一時停止) | 起動したスレッドのDeferredインスタンス ※DeferredはJobのサブクラス |
runBlocking | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値あり スレッドをブロックする(休止) | タスクブロックの戻り値 |
また、asyncはCoroutineScopeに定義された拡張関数です。
public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T> { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyDeferredCoroutine(newContext, block) else DeferredCoroutine<T>(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
スレッドの起動(第3引数)
スレッドの起動はasyncの引数(第3引数)にラムダ式(タスクブロック)を指定して実行するだけです。※引数の最後がラムダ式の場合、「( )」の外に出せる
この場合、起動されるスレッドはCoroutineScopeに格納されたCoroutineContextに従います。
// CoroutineScopeのCoroutineContextに従い、Defaultスレッドを起動 scope = SampleScope() scope?.async { // asyncビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") println("Time = ${getTime()} End") }
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 = 2973 Start Thread = DefaultDispatcher-worker-1 Time = 3975 ResultA = AAA Time = 3975 End
asyncは入れ子が可能です。子コルーチンが開始できます。
// ビルダーの入れ子 scope = SampleScope() scope?.async { // asyncビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") async { // asyncビルダー val _resultB = taskB() println("Time = ${getTime()} ResultB = ${_resultB}") val _resultC = taskC() println("Time = ${getTime()} ResultC = ${_resultC}") } val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") println("Time = ${getTime()} End") }
Time = 0129 Start Thread = DefaultDispatcher-worker-1 Time = 1132 ResultA = AAA Time = 1132 End Time = 2132 ResultB = BBB Time = 5139 ResultC = CCC
スレッドの切り替え(第1引数)
asyncビルダーの第1引数はCoroutineContextです。。
ここに新たなCoroutineContextを指定すると、CoroutineScopeに格納されたCoroutineContexへ加算(上書き)されます。つまり、引数で指定されたCoroutineContextが優先されます。
次の例は、Dispatchers.Mainを指定することで、スレッドプールをDefaultからMainに切り替える例です。asyncによりMainスレッドが起動されます。
※CoroutineContextの詳細は「Kotlin:コルーチン(Coroutine)」を参照
// ビルダーの引数でプールを指定、Mainスレッドで起動 scope = SampleScope() scope?.async(Dispatchers.Main) { // asyncビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") println("Time = ${getTime()} End") }
Time = 6439 Start Thread = main Time = 7442 ResultA = AAA Time = 7443 End
タスク戻り値の取得
asyncビルダーは戻り値にDeferredクラスのインスタンスを返します。
Deferred#await( )関数を使ってタスクブロックの戻り値を取得できます。
// タスクの戻り値を取得 scope = SampleScope() scope?.launch(Dispatchers.Main) { // asyncビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _deferredA = async(Dispatchers.Default) { // asyncビルダー taskA() // {...}内はラムダ式 } val _resultA = _deferredA.await() println("Time = ${getTime()} ResultB = ${_resultA}") println("Time = ${getTime()} End") }
ラムダ式の戻り値はタスクブロック内で最後に評価した値になります。ですので、この例はtaskA( )を実行した結果が戻り値です。
Time = 5106 Start Thread = main Time = 6111 ResultB = AAA Time = 6111 End
await( )はSuspend関数です。自身の実行後にスレッドを一時停止(Suspend)します。
そして、コルーチンの処理が完了したところでスレッドを再取得して、続きの処理を再開します。
この続きの処理を再開する時、コルーチンのタスクブロックの戻り値が受け渡されます。
※Suspend関数の詳細は「Coroutine:Suspend関数とその仕組み」を参照
ちなみに、DeferredはJobのサブクラスです。
public interface Deferred<out T> : Job { ... public suspend fun await(): T ... }
並列処理(Parallel)
asyncは実行される毎にスレッドを起動します。よって、連続して実行すれば、複数のスレッドが起動して並列処理になります。
ただし、並列に動作するスレッド数は、プールが管理できる数に制限されます。
※DefaultブールはCore数まで管理可能
// 並列処理 scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _deferredA = async(Dispatchers.Default) { // asyncビルダー taskA() } val _deferredB = async(Dispatchers.Default) { // asyncビルダー taskB() } val _deferredC = async(Dispatchers.Default) { // asyncビルダー taskC() } val _resultA = _deferredA.await() println("Time = ${getTime()} ResultA = ${_resultA}") val _resultB = _deferredB.await() println("Time = ${getTime()} ResultB = ${_resultB}") val _resultC = _deferredC.await() println("Time = ${getTime()} ResultC = ${_resultC}") println("Time = ${getTime()} End") }
Time = 0968 Start Thread = main Time = 1980 ResultA = AAA Time = 2985 ResultB = BBB Time = 3992 ResultC = CCC Time = 3992 End
逐次処理(Serial)
Deferred#await( )はタスクブロックの終了を待ちます。await( )を使えば逐次処理が出来ます。
// 逐次処理 scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _resultA = async(Dispatchers.Default) { // asyncビルダー taskA() }.await() println("Time = ${getTime()} ResultA = ${_resultA}") val _resultB = async(Dispatchers.Default) { // asyncビルダー taskB() }.await() println("Time = ${getTime()} ResultB = ${_resultB}") val _resultC = async(Dispatchers.Default) { // asyncビルダー taskC() }.await() println("Time = ${getTime()} ResultC = ${_resultC}") println("Time = ${getTime()} End") }
Time = 0707 Start Thread = main Time = 1711 ResultA = AAA Time = 3716 ResultB = BBB Time = 6720 ResultC = CCC Time = 6720 End
待ち合せ
awaitAll( )はタスクブロックの終了を待つ関数です。
引数は可変長引数になっており、複数のDeferredインスタンスが指定できます。つまり、複数のタスクの終了を待ちます。
awaitAll( )を使えば、「タスクAとBの終了を待ってタスクCの処理を行う」と言った、待ち合せを表現できます。
// タスク終了の待ち合せ scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _deferredA = async(Dispatchers.Default) { taskA() } val _deferredB = async(Dispatchers.Default) { taskB() } val _resultList = awaitAll(_deferredA, _deferredB) println("Time = ${getTime()} ResultA = ${_resultList[0]}") println("Time = ${getTime()} ResultB = ${_resultList[1]}") val _deferredC = async(Dispatchers.Default) { taskC() } val _resultC = _deferredC.await() println("Time = ${getTime()} ResultC = ${_resultC}") println("Time = ${getTime()} End") }
Time = 1958 Start Thread = main Time = 3976 ResultA = AAA Time = 3976 ResultB = BBB Time = 6980 ResultC = CCC Time = 6980 End
awaitAll( )の場合、タスクブロックの戻り値はListになるので注意が必要です。
引数:Deferredのインスタンスの順番が、戻り値:Listのインデックスの順番になります。
スタートオプション(第2引数)
asyncビルダーの第2引数はスタートオプションです。
コルーチンを開始する時の動作が指定できます。
スタートオプション CoroutineStart.XXX | コルーチン 開始のタイミング | コルーチン キャンセルのタイミング |
---|---|---|
DEFAULT (引数なし) | ビルダーの実行後、直ちに開始する | 開始時にキャンセル可能 起動中にキャンセル可能 |
LAZY | ビルダーの実行後、開始しない Job#start( )・Deferred#await( )で開始 | DEFAULTと同じ |
ATOMIC | DEFAULTと同じ | 開始時にキャンセル不可 起動中にキャンセル可能 |
UNDISPATCHED | DEFAULTと同じ | DEFAULTと同じ |
起動されるスレッドは Dispatchers.Unconfinedを指定して開始されるコルーチンと同じ |
引数がなければデフォルトのCroutineStart.DEFAULTが指定された事になります(拡張関数の定義より)。
CoroutineStart.LAZY
オプションLAZYはasyncビルダーの実行後にコルーチンを開始しない動作です。
コルーチンの開始はDeferred#await( )関数(またはJob#start( )関数)によって行われます。
var _scope: CoroutineScope? = null var _deferred: Deferred<String>? = null findViewById<Button>(R.id.btnReady).setOnClickListener { println("Time = ${getTime()} Ready coroutine !!") _scope = SampleScope() _scope?.launch(Dispatchers.Main) { _deferred = async(Dispatchers.Default, CoroutineStart.LAZY) { println("Time = ${getTime()} Start TaskC Thread = ${getThread()}") val _resultC = taskC() println("Time = ${getTime()} End TaskC") _resultC } } } findViewById<Button>(R.id.btnStart).setOnClickListener { println("Time = ${getTime()} Start coroutine !!") _scope?.launch(Dispatchers.Main) { val _resultC = _deferred?.await() println("Time = ${getTime()} TaskC Result = ${_resultC}") } }
fun taskC(): String { Thread.sleep(3000) // 重い処理の代わり return "CCC" // 重い処理の戻り値 }
Time = 1986 Ready coroutine !! ... Readyボタン押下 : 〔時間経過〕 : Time = 4625 Start coroutine !! ... Startボタン押下 Time = 4629 Start TaskC Thread = DefaultDispatcher-worker-1 ... Job#Start( )で開始 Time = 7631 End TaskC Time = 7632 TaskC Result = CCC
LAZYに対しDEFAULTはasyncビルダーの実行後にコルーチンを開始する動作です。
var _scope: CoroutineScope? = null var _deferred: Deferred<String>? = null findViewById<Button>(R.id.btnReady).setOnClickListener { println("Time = ${getTime()} Ready coroutine !!") _scope = SampleScope() _scope?.launch(Dispatchers.Main) { _deferred = async(Dispatchers.Default, CoroutineStart.DEFAULT) { println("Time = ${getTime()} Start TaskC Thread = ${getThread()}") val _resultC = taskC() println("Time = ${getTime()} End TaskC") _resultC } } } findViewById<Button>(R.id.btnStart).setOnClickListener { println("Time = ${getTime()} Start coroutine !!") _scope?.launch(Dispatchers.Main) { val _resultC = _deferred?.await() println("Time = ${getTime()} TaskC Result = ${_resultC}") } }
Time = 3080 Ready coroutine !! ... Readyボタン押下 Time = 3087 Start TaskC Thread = DefaultDispatcher-worker-2 ... 直ちに開始 Time = 6090 End TaskC Time = 9352 Start coroutine !! ... Startボタン押下 Time = 9356 TaskC Result = CCC ... 処理済み結果を取得
CoroutineStart.ATOMIC
オプションATOMICはコルーチンの開始時にキャンセルしない動作です。
ただし、起動中のコルーチンはキャンセルできます。
以下のサンプルはDefaultプールを使って、10個のコルーチンの開始を試みています。しかし、プールで管理可能なスレッドは4つのため、直ちに開始されるコルーチンはTaskC_0,1,2,3になり、残りはスレッドが空くまで待機します。
この待機中にキャンセルの発行を行ったとしても、ATOMICの場合は待機中のコルーチンをキャンセルしません。スレッドに空きが出ればコルーチンを開始します。
var _scope: CoroutineScope? = null findViewById<Button>(R.id.btnStart).setOnClickListener { _scope = SampleScope() _scope?.launch(Dispatchers.Main) { for(i in 0..9) { println("Time = ${getTime()} TaskC_${i} Start") async(Dispatchers.Default, CoroutineStart.ATOMIC) { if(isActive) { val _resultC = taskC() println("Time = ${getTime()} TaskC_${i} = ${_resultC}") } else println("Time = ${getTime()} TaskC_${i} Cancel!") } } } } findViewById<Button>(R.id.btnCancel).setOnClickListener { _scope?.cancel() }
Time = 5308 TaskC_0 Start Time = 5312 TaskC_1 Start Time = 5316 TaskC_2 Start Time = 5318 TaskC_3 Start Time = 5321 TaskC_4 Start Time = 5321 TaskC_5 Start Time = 5322 TaskC_6 Start Time = 5323 TaskC_7 Start Time = 5323 TaskC_8 Start Time = 5323 TaskC_9 Start : 〔キャンセル発行〕 : Time = 8317 TaskC_0 = CCC ... コルーチン開始、taskC( )が起動済み Time = 8318 TaskC_4 Cancel! ... コルーチン開始、キャンセル状態⇒強制終了 Time = 8319 TaskC_5 Cancel! Time = 8320 TaskC_6 Cancel! Time = 8320 TaskC_7 Cancel! Time = 8321 TaskC_8 Cancel! Time = 8321 TaskC_1 = CCC Time = 8322 TaskC_9 Cancel! Time = 8324 TaskC_2 = CCC Time = 8324 TaskC_3 = CCC
ATOMICに対しDEFAULTは待機中のコルーチンのキャンセルが行われます。
var _scope: CoroutineScope? = null findViewById<Button>(R.id.btnStart).setOnClickListener { _scope = SampleScope() _scope?.launch(Dispatchers.Main) { for(i in 0..9) { println("Time = ${getTime()} TaskC_${i} Start") async(Dispatchers.Default, CoroutineStart.DEFAULT) { if(isActive) { val _resultC = taskC() println("Time = ${getTime()} TaskC_${i} = ${_resultC}") } else println("Time = ${getTime()} TaskC_${i} Cancel!") } } } } findViewById<Button>(R.id.btnCancel).setOnClickListener { _scope?.cancel() }
Time = 2062 TaskC_0 Start Time = 2071 TaskC_1 Start Time = 2075 TaskC_2 Start Time = 2076 TaskC_3 Start Time = 2080 TaskC_4 Start Time = 2081 TaskC_5 Start Time = 2082 TaskC_6 Start Time = 2084 TaskC_7 Start Time = 2085 TaskC_8 Start Time = 2085 TaskC_9 Start : 〔キャンセル発行〕 : Time = 5077 TaskC_0 = CCC ... コルーチン開始、taskC( )が起動済み Time = 5077 TaskC_1 = CCC Time = 5092 TaskC_2 = CCC Time = 5093 TaskC_3 = CCC ... 残りはコルーチン開始時にキャンセル
関連記事: