コルーチン(Coroutine)は「非同期処理の手法」の1つです。
Kotlinが提供します。
コルーチンはビルダー(Builder)により開始されます。
ビルダーは3つの種類があり、その中の1つがlaunchです。
このlaunchビルダーについて、まとめます。
目次
ビルダーとは
ビルダー(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を定義 }
launchの特徴
ビルダーは3つの種類があります。
その中のlaunchはタスクブロックの戻り値を返さないビルダーです。
ビルダー | 概要 | 関数の戻り値 |
---|---|---|
launch | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値なし スレッドをブロックしない(一時停止) | 起動したスレッドのJobインスタンス |
async | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値あり スレッドをブロックしない(一時停止) | 起動したスレッドのDeferredインスタンス ※DeferredはJobのサブクラス |
runBlocking | スレッドを起動してタスクブロックを実行 タスクブロックの戻り値あり スレッドをブロックする(休止) | タスクブロックの戻り値 |
また、launchはCoroutineScopeに定義された拡張関数です。
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
スレッドの起動(第3引数)
スレッドの起動はlaunchの引数(第3引数)にラムダ式(タスクブロック)を指定して実行するだけです。※引数の最後がラムダ式の場合、「( )」の外に出せる
この場合、起動されるスレッドはCoroutineScopeに実装されたCoroutineContextに従います。
// CoroutineScopeのCoroutineContextに従い、Defaultスレッドを起動 scope = SampleScope() scope?.launch { // launchビルダー 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 = 3901 Start Thread = DefaultDispatcher-worker-1 Time = 4910 ResultA = AAA Time = 4911 End
launchは入れ子が可能です。子コルーチンが開始できます。
// ビルダーの入れ子 scope = SampleScope() scope?.launch { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") launch { // launchビルダー 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 = 9137 Start Thread = DefaultDispatcher-worker-2 Time = 0142 ResultA = AAA Time = 0142 End Time = 1141 ResultB = BBB Time = 4143 ResultC = CCC
スレッドの切り替え(第1引数)
launchビルダーの第1引数はCoroutineContextです。
ここに新たなCoroutineContextを指定すると、CoroutineScopeに格納されたCoroutineContexへ加算(上書き)されます。つまり、引数で指定されたCoroutineContextが優先されます。
次の例は、Dispatchers.Mainを指定することで、スレッドプールをDefaultからMainに切り替える例です。launchによりMainスレッドが起動されます。
※CoroutineContextの詳細は「Kotlin:コルーチン(Coroutine)」を参照
// ビルダーの引数でプールを指定、Mainスレッドで起動 scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") println("Time = ${getTime()} End") }
Time = 3092 Start Thread = main Time = 4095 ResultA = AAA Time = 4095 End
並列処理(Parallel)
launchは実行される毎にスレッドを起動します。よって、連続して実行すれば、複数のスレッドが同時に起動して並列処理になります。
ただし、並列に動作するスレッド数は、プールが管理できる数に制限されます。
※DefaultブールはCore数まで管理可能
// 並列処理 scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") launch(Dispatchers.Default) { // launchビルダー val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") } launch(Dispatchers.Default) { // launchビルダー val _resultB = taskB() println("Time = ${getTime()} ResultB = ${_resultB}") } launch(Dispatchers.Default) { // launchビルダー val _resultC = taskC() println("Time = ${getTime()} ResultC = ${_resultC}") } println("Time = ${getTime()} End") }
Time = 2808 Start Thread = main Time = 2810 End Time = 3811 ResultA = AAA Time = 4811 ResultB = BBB Time = 5811 ResultC = CCC
逐次処理(Serial)
Job#join( )はタスクブロックの終了を待ちます。join( )を使えば逐次処理が出来ます。
// 逐次処理 scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") launch(Dispatchers.Default) { // launchビルダー val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") }.join() launch(Dispatchers.Default) { // launchビルダー val _resultB = taskB() println("Time = ${getTime()} ResultB = ${_resultB}") }.join() launch(Dispatchers.Default) { // launchビルダー val _resultC = taskC() println("Time = ${getTime()} ResultC = ${_resultC}") }.join() println("Time = ${getTime()} End") }
Time = 1713 Start Thread = main Time = 2716 ResultA = AAA Time = 4719 ResultB = BBB Time = 7724 ResultC = CCC Time = 7725 End
join( )はSuspend関数です。自身の実行後にスレッドを一時停止(Suspend)します。
そして、子コルーチンの処理が完了したところで、一時停止していたスレッドを再取得して、タスクブロックの続きの処理を再開します。
※Suspend関数の詳細は「Coroutine:Suspend関数とその仕組み」を参照
待ち合せ
joinAll( )はタスクブロックの終了を待つ関数です。
引数は可変長引数になっており、複数のJobインスタンスが指定できます。つまり、複数のタスクの終了を待ちます。
joinAll( )を使えば、「タスクAとBの終了を待ってタスクCの処理を行う」と言った、待ち合せを表現できます。
// タスク終了の待ち合せ( 1 ) scope = SampleScope() scope?.launch(Dispatchers.Main) { // launchビルダー println("Time = ${getTime()} Start Thread = ${getThread()}") val _jobA = launch(Dispatchers.Default) { // launchビルダー val _resultA = taskA() println("Time = ${getTime()} ResultA = ${_resultA}") } val _jobB = launch(Dispatchers.Default) { // launchビルダー val _resultB = taskB() println("Time = ${getTime()} ResultB = ${_resultB}") } joinAll(_jobA, _jobB) launch(Dispatchers.Default) { val _resultC = taskC() println("Time = ${getTime()} ResultC = ${_resultC}") } println("Time = ${getTime()} End") }
Time = 1171 Start Thread = main Time = 2184 ResultA = AAA Time = 3182 ResultB = BBB Time = 3183 End Time = 6184 ResultC = CCC
joinAll( )はSuspend関数です。自身の実行後にスレッドを一時停止(Suspend)します。
中身は単純で、引数のJobインスタンスに対して、Job#join( )を一つ一つ実行しているだけです。
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
※Suspend関数の詳細は「Coroutine:Suspend関数とその仕組み」を参照
スタートオプション(第2引数)
launchビルダーの第2引数はスタートオプションです。
コルーチンを開始する時の動作が指定できます。
スタートオプション CoroutineStart.XXX | コルーチン 開始のタイミング | コルーチン キャンセルのタイミング |
---|---|---|
DEFAULT (引数なし) | ビルダーの実行後、直ちに開始する | 開始時にキャンセル可能 起動中にキャンセル可能 |
LAZY | ビルダーの実行後、開始しない Job#start( )・Deferred#await( )で開始 | DEFAULTと同じ |
ATOMIC | DEFAULTと同じ | 開始時にキャンセル不可 起動中にキャンセル可能 |
UNDISPATCHED | DEFAULTと同じ | DEFAULTと同じ |
起動されるスレッドは Dispatchers.Unconfinedを指定して開始されるコルーチンと同じ |
引数がなければデフォルトのCroutineStart.DEFAULTが指定された事になります(拡張関数の定義より)。
CoroutineStart.LAZY
オプションLAZYはlaunchビルダーの実行後にコルーチンを開始しない動作です。
コルーチンの開始はJob#start( )関数によって行われます。
var _scope: CoroutineScope? = null var _job: Job? = null findViewById<Button>(R.id.btnReady).setOnClickListener { // Readyボタン println("Time = ${getTime()} Ready coroutine !!") _scope = SampleScope() _scope?.launch(Dispatchers.Main) { _job = launch(Dispatchers.Default, CoroutineStart.LAZY) { println("Time = ${getTime()} Start TaskC Thread = ${getThread()}") val _resultC = taskC() println("Time = ${getTime()} End TaskC Result = ${_resultC}") } } } findViewById<Button>(R.id.btnStart).setOnClickListener { // Startボタン println("Time = ${getTime()} Start coroutine !!") _job?.start() }
Time = 0827 Ready coroutine !! ... Readyボタン押下 : 〔時間経過〕 : Time = 4698 Start coroutine !! ... Startボタン押下 Time = 4710 Start TaskC Thread = DefaultDispatcher-worker-1 ... Job#Start( )で開始 Time = 7712 End TaskC Result = CCC
LAZYに対しDEFAULTはlaunchビルダーの実行後にコルーチンを開始する動作です。
var _scope: CoroutineScope? = null var _job: Job? = null findViewById<Button>(R.id.btnReady).setOnClickListener { println("Time = ${getTime()} Ready coroutine !!") _scope = SampleScope() _scope?.launch(Dispatchers.Main) { _job = launch(Dispatchers.Default, CoroutineStart.DEFAULT) { println("Time = ${getTime()} Start TaskC Thread = ${getThread()}") val _resultC = taskC() println("Time = ${getTime()} End TaskC Result = ${_resultC}") } } } findViewById<Button>(R.id.btnStart).setOnClickListener { println("Time = ${getTime()} Start coroutine !!") _job?.start() }
Time = 2276 Ready coroutine !! ... Readyボタン押下 Time = 2279 Start TaskC Thread = DefaultDispatcher-worker-1 ... 直ちに開始 Time = 5282 End 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("TaskC_${i} Start") launch(Dispatchers.Default, CoroutineStart.ATOMIC) { if(isActive) { val _resultC = taskC() println("TaskC_${i} = ${_resultC}") } else println("TaskC_${i} Cancel!") } } } } findViewById<Button>(R.id.btnCancel).setOnClickListener { _scope?.cancel() }
Time = 9730 TaskC_0 Start ... コルーチンの開始を試みる Time = 9737 TaskC_1 Start Time = 9745 TaskC_2 Start Time = 9747 TaskC_3 Start Time = 9750 TaskC_4 Start Time = 9751 TaskC_5 Start Time = 9751 TaskC_6 Start Time = 9751 TaskC_7 Start Time = 9752 TaskC_8 Start Time = 9752 TaskC_9 Start : 〔キャンセル発行〕 : Time = 2746 TaskC_0 = CCC ... コルーチン開始、taskC( )が起動済み Time = 2747 TaskC_4 Cancel! ... コルーチン開始、キャンセル状態⇒強制終了 Time = 2747 TaskC_5 Cancel! Time = 2747 TaskC_6 Cancel! Time = 2748 TaskC_7 Cancel! Time = 2748 TaskC_8 Cancel! Time = 2748 TaskC_1 = CCC Time = 2749 TaskC_9 Cancel! Time = 2762 TaskC_2 = CCC Time = 2763 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("TaskC_${i} Start") launch(Dispatchers.Default, CoroutineStart.DEFAULT) { if(isActive) { val _resultC = taskC() println("TaskC_${i} = ${_resultC}") } else println("TaskC_${i} Cancel!") } } } } findViewById<Button>(R.id.btnCancel).setOnClickListener { _scope?.cancel() }
Time = 5509 TaskC_0 Start Time = 5519 TaskC_1 Start Time = 5523 TaskC_2 Start Time = 5524 TaskC_3 Start Time = 5525 TaskC_4 Start Time = 5526 TaskC_5 Start Time = 5526 TaskC_6 Start Time = 5526 TaskC_7 Start Time = 5527 TaskC_8 Start Time = 5527 TaskC_9 Start : 〔キャンセル発行〕 : Time = 8522 TaskC_0 = CCC ... コルーチン開始、taskC( )が起動済み Time = 8524 TaskC_1 = CCC Time = 8526 TaskC_2 = CCC Time = 8526 TaskC_3 = CCC ... 残りはコルーチン開始時にキャンセル
関連記事: