Coroutine:asyncビルダーでコルーチン開始

投稿日:  更新日:

コルーチン(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"           // 重い処理の戻り値
    }
getThread, getTime
    inline private fun getTime(): String {
        return "%04d".format(System.currentTimeMillis() % 10000)
    }
    inline private fun getThread(): String {
        return Thread.currentThread().name
    }

asyncでスレッドの起動

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")
            }

asyncの入れ子

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")
            }

asyncでスレッドの起動

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")
            }

asyncによる並列処理

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")
            }

asyncによる逐次処理

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と同じ
ATOMICDEFAULTと同じ開始時にキャンセル不可
起動中にキャンセル可能
UNDISPATCHEDDEFAULTと同じ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
                            ... 残りはコルーチン開始時にキャンセル
スポンサーリンク

関連記事:

近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 特徴としてnon-blocking動作をサポートします。 このnon-blocking動作についてまとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンの構成要素であるSuspend関数について、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがlaunchです。 このlaunchビルダーについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するlaunchビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するasyncビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがrunBlockingです。 このrunBlockingビルダーについて、まとめます。 ...
CoroutineContextはコルーチンで起動されるスレッドの属性を格納しています。 その中にコルーチンの名前を表現するName属性があります。 Name属性を出力する方法を紹介します。 ...
コルーチン(Coroutine)は「非同期処理プログラミングの手法」の1つです。 Kotlinが提供します。 withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。 このwithContextについて、まとめます。 ...
コルーチン間でメッセージ(データのこと)の送受信を行うことが出来ます。 これにより、処理の投げっぱなしになりがちな非同期処理と、連携を強化できます。 しかも、ProduceやFlowを使うと記述が簡素になり、プログラミングの容易さと読みやすさが向上して便利です。 今回は、この「メッセージの送受信」について、使い方をまとめました。 ※環境:Android Studio Flamingo | 2022.2.1    :org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 ...
スポンサーリンク