1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlin のプレイグラウンドのコルーチンの概要

Last updated at Posted at 2024-03-06

はじめに

コルーチンの学習として、Kotlin のプレイグラウンドのコルーチンの概要というAndroidアプリ開発におけるコルーチンの基礎を説明してそうなCodelabを進めて理解を深めます。今回は特に、launch, async awaitについて、自身でもサンプルコードを記載していきながら理解を深めることに重きを置きます。

本文を記入しつつ、つまづいた部分や気になった部分は本文の間に挟む形で、メモ書き程度にここに記入していきます。

この記事がコルーチンの理解の手助けになれば幸いです。

1.始める前に

この Codelab では、優れたユーザー エクスペリエンスを提供するために Android デベロッパーが理解しておくべき重要なスキルである同時実行を紹介します。同時実行とは、アプリで複数のタスクを同時に行うことです。たとえば、アプリは、ユーザーの入力イベントに応答し、それに応じて UI を更新しながら、ウェブサーバーからデータを取得したり、デバイスにユーザーデータを保存したりできます。

アプリで同時に処理を行うには、Kotlin コルーチンを使用します。コルーチンを使用すると、コードブロックの実行を中断して後で再開できるため、その間に他の処理を行うことができます。コルーチンを使用すると、非同期コードを記述しやすくなります。つまり、あるタスクが終了してから次のタスクを開始する必要がなく、複数のタスクを同時に実行できます。

この Codelab では、Kotlin のプレイグラウンドの基本的な例を紹介し、非同期プログラミングに慣れるよう、コルーチンの実践演習を行います。

<メモ開始>
スクリーンショット 2024-03-08 11.58.48.png

もし処理①を中断してその間に処理②を実行し、それが終わったら処理①を再開するとしたら、利用者からみれば処理①②の同時実行ができているということなのか?
<メモ終了>

2.同期コード

同期コードで進行中の概念上のタスクは、一度に 1 つだけです。これは順次の直線的なパスと考えることができます。あるタスクが完全に終了してから次のタスクを開始する必要があります。同期コードの例を次に示します。

  1. Kotlin のプレイグラウンドを開きます。
  2. このコードを、晴れの天気予報を表示するプログラムのコードに置き換えます。main() 関数では、まず Weather forecast というテキストを出力します。次に Sunny を出力します。
fun main() {
    println("Weather forecast")
    println("Sunny")
}
  1. コードを実行します。上のコードを実行すると、出力は次のようになります。
Weather forecast
Sunny

テキストを出力するタスクが完了してから実行が次のコード行に移るため、println() は同期呼び出しです。main() の各関数呼び出しは同期的であるため、main() 関数全体が同期的です。関数が同期か非同期かは、その構成要素で決まります。

同期関数は、タスクが完了した場合にのみ戻ります。そのため、main() の最後の print ステートメントが実行された後、すべての処理が完了します。main() 関数が戻り、プログラムが終了します。

ここで、晴れの天気予報を取得するには、リモートのウェブサーバーへのネットワーク リクエストが必要だとします。晴れの天気予報を出力する前に、コードに遅延を追加して、ネットワーク リクエストをシミュレートします。

  1. まず、コードの先頭で main() 関数の前に import kotlinx.coroutines.* を追加します。これにより、使用する関数が Kotlin コルーチン ライブラリからインポートされます。
  2. コードを変更して delay(1000) 呼び出しを追加します。この呼び出しで、main() 関数の残りの部分の実行が 1000 ミリ秒(1 秒)遅れます。この delay() 呼び出しを、Sunny の print ステートメントの前に挿入します。
import kotlinx.coroutines.*

fun main() {
    println("Weather forecast")
    delay(1000)
    println("Sunny")
}

delay() は、実際は Kotlin コルーチン ライブラリが提供する特別な suspend 関数です。main() 関数の実行はこの時点で中断(一時停止)し、指定した遅延時間(この場合は 1 秒)が経過すると再開されます。

この時点でプログラムを実行しようとすると、コンパイル エラー「Suspend function 'delay' should be called only from a coroutine or another suspend function」が発生します。

Kotlin のプレイグラウンドでコルーチンを学習するために、コルーチン ライブラリの runBlocking() 関数の呼び出しで既存のコードをラップできます。runBlocking() はイベントループを実行し、再開の準備ができたときに各タスクを中断したところから続行することで、複数のタスクを一度に処理できます。
3. main() 関数の既存の内容を runBlocking {} 呼び出しの本体に移動します。runBlocking{} の本体は新しいコルーチン内で実行されます。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        delay(1000)
        println("Sunny")
    }
}

runBlocking() は同期的です。ラムダブロック内のすべての処理が完了するまで戻りません。つまり、delay() 呼び出しの処理が完了するまで(1 秒経過するまで)待機してから、Sunny print ステートメントの実行を続行します。runBlocking() 関数内のすべての処理が完了すると、関数が戻り、プログラムが終了します。

プログラムを実行します。出力は次のとおりです。

Weather forecast
Sunny

出力は前と同じです。コードはまだ同期的です。直線的に動作し、一度に 1 つのことしか行いません。しかし、遅延のために長時間にわたって実行される点が異なります。

コルーチンの「コ」は、協調的という意味です。何かを待つために中断するとき、コードは連携して基となるイベントループを共有し、その間に他の処理を行えるようにします(「コルーチン」の「ルーチン」は、関数などの一連の命令を意味します)。この例の場合、delay() 呼び出しに到達するとコルーチンが中断します。コルーチンが中断している 1 秒間に(このプログラムに他の処理はありませんが)他の処理を実施できます。遅延時間が経過すると、コルーチンの実行が再開し、Sunny が出力されます。

<メモ開始>
イベントループとはなんでしょうか?はじめて聞いた用語です。
イベントループを理解するためにこの記事が参考になりました。

また、イベントループとrunBlockingについて、chatGPTに聞いてみた結果が以下です。
screencapture-chat-openai-c-a5c30f47-e51e-4ff0-978d-3083efb9981b-2023-08-16-16_43_20 (1).png
<メモ終了>

注: 一般的に、このような main() 関数内で runBlocking() を使用することは、学習目的に限ります。Android には、準備が整ったときの再開を処理するためのイベントループが用意されているため、Android アプリのコードに runBlocking() は必要ありません。ただし、runBlocking() はテストには有用です。テストのアサーションを呼び出す前に、アプリの特定の条件を待たせることができます。

<メモ開始>
runBlockingは通常のアプリ開発では使わないのか、、なぜなのだろうか。

runBlockingを使うと任意コルーチンスコープ(viewModelScopemとか)を選べないから?
runBlockingにDispacher(Coroutines Context)(実行スレッドを指定するもの)を引数に渡せないから?
<メモ終了>

気象データを取得するためのネットワーク リクエストを実行する実際のロジックが複雑になる場合は、ロジックを独自の関数に抜き出すことをおすすめします。コードをリファクタリングして効果を確認してみましょう。

  1. 気象データのネットワーク リクエストをシミュレートするコードを抜き出し、printForecast() という独自の関数に移します。runBlocking() のコードから printForecast() を呼び出します。
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

fun printForecast() {
    delay(1000)
    println("Sunny")
}

プログラムを実行すると、先ほどと同じコンパイル エラーが表示されます。suspend 関数はコルーチンまたは別の suspend 関数からしか呼び出せないため、printForecast() を suspend 関数として定義します。
2. printForecast() 関数宣言の fun キーワードの直前に suspend 修飾子を追加して、suspend 関数にします。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

delay() は suspend 関数でした。今回、printForecast() も suspend 関数になりました。

suspend 関数は通常の関数に似ていますが、中断して後で再開できます。そのために、suspend 関数は、この機能を利用できるようにする他の suspend 関数からしか呼び出すことができません。

suspend 関数には 0 個以上の中断ポイントを含めることができます。中断ポイントは、関数の実行を中断できる、関数内の場所です。実行が再開されると、コード内の最後に中断したところから再開して、関数の残りの部分に進みます。
3. 練習として、コードの printForecast() 関数の宣言の下に、別の suspend 関数を追加します。この新しい suspend 関数を printTemperature() とします。天気予報の気温データを取得するためのネットワーク リクエストを行うように見せることができます。
関数内でも実行を 1000 ミリ秒遅らせ、気温値を摂氏 30 度のように出力します。エスケープ シーケンス "\u00b0" を使用して度記号 ° を出力できます。

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

4.main() 関数の runBlocking() コードから新しい printTemperature() 関数を呼び出します。コード全体を次に示します。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
        printTemperature()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

5.プログラムを実行します。出力は次のようになります。

Weather forecast
Sunny
30°C

このコードでは、まず printForecast() suspend 関数の遅延でコルーチンが中断し、その 1 秒間の遅延後に再開します。Sunny テキストが出力されます。printForecast() 関数は呼び出し元に戻ります。

次に、printTemperature() 関数が呼び出されます。このコルーチンは、delay() の呼び出しに到達すると中断し、1 秒後に再開して気温値を出力し終えます。printTemperature() 関数はすべての処理を完了して戻ります。

runBlocking() 本体にはこれ以上実行するタスクがないため、runBlocking() 関数が戻り、プログラムは終了します。

前述のように、runBlocking() は同期的であり、本体内の各呼び出しは順次呼び出されます。なお、適切に設計された suspend 関数は、すべての処理が完了してから戻ります。その結果、suspend 関数は順次実行されます。

6.(省略可)このプログラムを遅延させて実行するためにかかる時間を確認する場合は、コードを measureTimeMillis() の呼び出しでラップすると、渡されたコードブロックの実行にかかった時間(ミリ秒)が返されます。この関数にアクセスするためにインポート ステートメント(import kotlin.system.*)を追加します。実行時間を出力し、1000.0 で割ってミリ秒を秒に変換します。

import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            printForecast()
            printTemperature()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

出力

Weather forecast
Sunny
30°C
Execution time: 2.128 seconds

出力は、実行に約 2.1 秒かかったことを示しています(正確な実行時間は若干異なる可能性があります)。各 suspend 関数で 1 秒の遅延が発生するため、妥当なようです。

ここまでで、コルーチン内のコードがデフォルトで順次呼び出されることを確認しました。同時実行を行うには、明示的に指定する必要があります。その方法については、次のセクションで説明します。協調的なイベントループを利用して複数のタスクを同時に実行すると、プログラムの実行時間を短縮できます。

3.非同期コード

コルーチン ライブラリの launch() 関数を使用して、新しいコルーチンを起動します。タスクを同時に実行するには、複数の launch() 関数をコードに追加して、複数のコルーチンを同時進行できるようにします。

Kotlin のコルーチンは、構造化された同時実行という重要なコンセプトに従っています。同時実行を明示的に要求しない限り(launch() を使用するなど)、コードはデフォルトで順次処理され、基となるイベントループと連携します。関数を呼び出す場合は、実装の詳細で使用したコルーチンの数に関係なく、関数が戻るまでにその処理を完全に終了することが前提となります。例外で失敗しても、例外がスローされると、関数から保留中のタスクはなくなります。したがって、例外がスローされても、処理が正常に完了しても、制御フローが関数から戻ればすべての処理が終了します。

1.前のステップのコードから始めます。launch() 関数を使用して、printForecast() と printTemperature() の各呼び出しをそれぞれのコルーチンに移動します。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

プログラムを実行します。出力は次のとおりです。

Weather forecast
Sunny
30°C

出力は同じですが、プログラムの実行が速くなったことにお気づきでしょうか。以前は、suspend 関数 printForecast() が完全に終了するまで待ってから printTemperature() 関数に移動する必要がありました。今回、printForecast() と printTemperature() は、別々のコルーチン内にあるため同時に実行できるようになりました。

スクリーンショット 2024-03-08 12.30.55.png

launch { printForecast() } 呼び出しは、printForecast() のすべての処理が完了する前に戻ることができます。これがコルーチンの優れている点です。次の launch() 呼び出しに移動して、次のコルーチンを開始できます。すべての処理が完了する前でも、launch { printTemperature() } も同様に戻ります。

(省略可)プログラムがどれくらい速くなったのかを確認する場合は、measureTimeMillis() のコードを追加して実行時間を確認します。

import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
   val time = measureTimeMillis {
       runBlocking {
           println("Weather forecast")
           launch {
               printForecast()
           }
           launch {
               printTemperature()
           }
       }
   }
   println("Execution time: ${time / 1000.0} seconds")
}
...
出力:


Weather forecast
Sunny
30°C
Execution time: 1.122 seconds

実行時間が約 2.1 秒から約 1.1 秒に短縮されたことがわかります。同時実行オペレーションを追加すると、プログラムの実行が速くなります。この時間測定コードを削除してから、次のステップに進んでください。

<メモ開始>

import kotlinx.coroutines.*

    fun main() {
        runBlocking {
            async { getForecast() }
            println("コードラボ Weather forecast")
        }
    }

    suspend fun getForecast() {
        delay(1000)
        println("コードラボ Sunny")
    }
    
2023-09-21 20:20:48.387 29843-29843 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Weather forecast
2023-09-21 20:20:49.388 29843-29843 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Sunny

launchと同じように、asyncも一つでも入れるとコードが同時実行される。(asyncを先に書いているのに、後ろのprintlnが先に終了している。)
<メモ終了>

2 回目の launch() 呼び出しの後、runBlocking() コードの末尾より前に、別の print ステートメントを追加するとどうなるでしょうか?メッセージは出力のどこに表示されますか?

runBlocking() コードを変更して、そのブロックの末尾より前に print ステートメントを追加します。

...

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
        println("Have a good day!")
    }
}
...
プログラムを実行すると、次のように出力されます。

Weather forecast
Have a good day!
Sunny
30°C

この出力から、printForecast() と printTemperature() の 2 つの新しいコルーチンが起動した後、Have a good day! を出力する次の命令に進めることがわかります。これは、launch() の「ファイア アンド フォーゲット(撃ちっぱなし)」の性質を表しています。launch() で新しいコルーチンを起動します。処理がいつ終了するのかを気にする必要はありません。

<メモ開始>

import kotlinx.coroutines.*

    fun main() {
        runBlocking {
            println("コードラボ Weather forecast")
            val forecast: Deferred<String> = async {
                getForecast()
            }
            val temperature: Deferred<String> = async {
                getTemperature()
            }
            println("コードラボ test")
            println("${forecast.await()} ${temperature.await()}")
            println("コードラボ Have a good day!")
        }
    }

    suspend fun getForecast(): String {
        delay(1000)
        return "コードラボ Sunny"
    }

    suspend fun getTemperature(): String {
        delay(1000)
        return "30\u00b0C"
    }

2023-09-21 19:55:48.231 28684-28684 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Weather forecast
2023-09-21 19:55:48.231 28684-28684 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ test
2023-09-21 19:55:49.233 28684-28684 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Sunny 30°C
2023-09-21 19:55:49.233 28684-28684 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Have a good day!

asyncを利用することで、時間のかかる処理(getForecast, getTemperature)を同時実行できるため二つの処理時間が1秒で済んでいる。 また、acync, awaitを使うことで戻り値付きのsuspend関数を扱え、戻り値を受け取った値.awaitとすることで値に何か代入されたタイミングで処理を実行することができる。

awaitの後ろの処理が長い時間のかかる処理でなかったとしても、await部分が終わるまではその後ろは実行されない 今回の例だと、awaitの後ろのprintlnはgetForecast等より時間がかからず先に終わるはずだが、awaitの部分が実行完了してからその後にprintlnが実行されている。
<メモ終了>

その後、コルーチンが処理を完了し、残りの出力ステートメントを出力します。runBlocking() 呼び出しの本体の処理(すべてのコルーチンを含む)がすべて完了すると、runBlocking() が戻り、プログラムが終了します。

これで、同期コードが非同期コードに変わりました。非同期関数が戻ったとき、タスクはまだ終了していない可能性があります。launch() の場合がそうでした。関数は戻りましたが、その処理はまだ完了していません。launch() を使用すると、コード内で複数のタスクを同時に実行できます。これは、開発する Android アプリで使用できる強力な機能です。

実際のところ、予報と気温のネットワーク リクエストに要する時間は不明です。両方のタスクが完了したときに統一された天気予報を表示する場合、現在の launch() によるアプローチでは不十分です。そこで async() を使用します。

コルーチンの終了タイミングを重視しており、コルーチンからの戻り値が必要な場合は、コルーチン ライブラリの async() 関数を使用します。

async() 関数は Deferred 型のオブジェクトを返します。これは、準備ができたらそこに結果が入るという約束のようなものです。await() を使用して Deferred オブジェクトの結果にアクセスできます。

まず、予報と気温のデータを出力するのではなく、String を返すように suspend 関数を変更します。関数名を printForecast() と printTemperature() から getForecast() と getTemperature() に更新します。

...

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

2 つのコルーチンに launch() ではなく async() を使用するように runBlocking() コードを変更します。各 async() 呼び出しの戻り値を、forecast と temperature という変数に格納します。これらは、String 型の結果を保持する Deferred オブジェクトです(Kotlin の型推論により型の指定は省略できますが、async() 呼び出しによって返される内容が明確になるよう以下に記載します)。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        ...
    }
}
...

コルーチンの後半で、2 つの async() 呼び出しの後、Deferred オブジェクトに対して await() を呼び出すことで、それぞれのコルーチンの結果にアクセスできます。この場合、forecast.await() と temperature.await() を使用して各コルーチンの値を出力できます。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

プログラムを実行すると、出力は次のようになります。

Weather forecast
Sunny 30°C
Have a good day!
ここでは、同時実行して予報と気温データを取得する 2 つのコルーチンを作成しました。それぞれが完了すると値を返しました。その後、2 つの戻り値を 1 つの print ステートメント Sunny 30°C にまとめました。

注: async(), の実例については、Now in Android アプリの該当部分をご覧ください。SyncWorker クラスで、sync() の呼び出しは、特定のバックエンドへの同期が成功した場合にブール値を返します。いずれかの同期オペレーションが失敗した場合、アプリは再試行する必要があります。

<メモ開始>

// launchをawaitの前に入れた場合
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("コードラボ Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        launch {
            printTest()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("コードラボ Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "コードラボ Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

suspend fun printTest() {
    delay(3000)
    println("コードラボ test")
}

2023-09-21 20:11:47.742 28976-28976 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Weather forecast
2023-09-21 20:11:48.746 28976-28976 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Sunny 30°C
2023-09-21 20:11:48.746 28976-28976 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Have a good day!
2023-09-21 20:11:50.751 28976-28976 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ test

今まではlaunchを一つでも入れると記載させている処理すべてが同時に実行されるという理解だった。 左のコードだと全ての処理が同時実行されるのでawaitの後ろのprintlnも同時実行されlaunchやasync, awaitより先に処理が終わるという理解だった。

しかし、実際にはawaitの後ろのprintlnはawaitの後に実行された。 launchの処理が終わったタイミングは一番初めのprintlnから3秒後となっているため、awaitより上の処理は同時実行されていることが分かるが、awaitより下の処理は同時実行されていない。

// launchをawaitの後に入れた場合
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("コードラボ Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        launch {
            printTest()
        }
        println("コードラボ Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "コードラボ Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

suspend fun printTest() {
    delay(3000)
    println("コードラボ test")
}


2023-09-21 20:14:44.319 29092-29092 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Weather forecast
2023-09-21 20:14:45.321 29092-29092 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Sunny 30°C
2023-09-21 20:14:45.321 29092-29092 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ Have a good day!
2023-09-21 20:14:48.326 29092-29092 System.out              com...id.wear.apps.lab.nsakai.debug  I  ドラボ test

今まではlaunchを一つでも入れると記載させている処理すべてが同時に実行されるという理解だった。 左のコードだと全ての処理が同時実行されるので、awaitの後ろのlaunchとprintlnも同時実行されawaitより上の処理と実行開始タイミングが同じになるという理解だった。

しかし、実際にはawaitの後ろのlaunchとprintlnはawaitの後に実行された。 launchの処理が終わったタイミングはawaitが終了した3秒後となっているため、awaitより下の処理は同時実行されていないことが分かる。
<メモ終了>

この天気の例をさらに 1 歩進めて、コルーチンが処理の並列分解にどのように役立つのかを見てみましょう。並列分解では、問題を小さなサブタスクに分割して、並列に解けるようにします。サブタスクの結果が揃ったら、まとめて最終的な結果を出すことができます。

コードで、runBlocking() の本体から天気予報のロジックを抜き出して、Sunny 30°C という文字列の組み合わせを返す単一の getWeatherReport() 関数にします。

コードで新しい suspend 関数 getWeatherReport() を定義します。
この関数を、空のラムダブロックを指定した coroutineScope{} の呼び出しと結果が等しくなるように設定します。最終的に、天気予報を取得するロジックが含まれることになります。

...

suspend fun getWeatherReport() = coroutineScope {

}

...

coroutineScope{} は、この天気予報タスクのローカル スコープを作成します。このスコープ内で起動されたコルーチンは、このスコープ内でグループ化されます。このキャンセルと例外の意味については後述します。

coroutineScope() の本体内で、async() を使用して 2 つの新しいコルーチンを作成し、それぞれ予報と気温データを取得します。この 2 つのコルーチンの結果を組み合わせて、天気予報文字列を作成します。そのためには、async() 呼び出しによって返された各 Deferred オブジェクトに対して await() を呼び出します。これにより、この関数から戻る前に、各コルーチンがその処理を完了し、結果を返すようになります。

...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

...

この新しい getWeatherReport() 関数を runBlocking() から呼び出します。コード全体を次に示します。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

プログラムを実行すると、次の出力が表示されます。

Weather forecast
Sunny 30°C
Have a good day!

出力は同じですが、ここで注意すべき重要な点がいくつかあります。前述のように、coroutineScope() は、起動したコルーチンを含むすべての処理が完了した場合にのみ返されます。この場合、コルーチン getForecast() と getTemperature() の両方が終了し、それぞれの結果を返す必要があります。その後、Sunny テキストと 30°C が組み合わされ、スコープから返されます。この Sunny 30°C という天気予報が出力され、呼び出し元は Have a good day! という最後の print ステートメントに進むことができます。

coroutineScope() は、関数が内部で処理を同時実行していても、coroutineScope はすべての処理が完了するまでは戻らないため、呼び出し元には同期オペレーションに見えます。

構造化された同時実行に関する重要な点は、複数の同時実行オペレーションを、単一の同期オペレーションにまとめることができるということです。同時実行は実装の詳細です。呼び出しコードの唯一の要件は、suspend 関数またはコルーチンであることです。それ以外には、呼び出しコードの構造で同時実行の詳細を考慮する必要はありません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?