0
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?

More than 1 year has passed since last update.

コルーチンとチャネル入門#4 suspend関数の使用

Last updated at Posted at 2022-06-07

ソース記事はこちら
Retrofitにはコルーチンに対するネイティブなサポートが提供されているため、それを使うことにする。Call<List<Repo>>を返却する代わりに、今ではsuspend関数としてAPI呼び出しを定義する。

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(
        @Path("org") org: String
    ): List<Repo>
}

その裏にある主な考えは、要求を実行するためにsuspend関数を使うときは、配下のスレッドがブロッキングされないことである。まさにそれがどのように動くか、少し後で説明する。
留意すべきは、今ではgetOrgReposは、Callを返却する代わりに、直接結果を返却する。もし結果が成功でない場合は、例外がスローされる。
あるいは、RetrofitはResponse内に結果をラップして返却することもできる。このケースでは、結果本体は提供され、手動でエラーをチェックすることができる。このチュートリアルではReponseバージョンを使用する。
GitHubServiceにどうか次の宣言を追加してもらいたい。

interface GitHubService {
    // getOrgReposCall と getRepoContributorsCall 宣言 ※以下のメソッドを追加する
    
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(
        @Path("org") org: String
    ): Response<List<Repo>>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    suspend fun getRepoContributors(
        @Path("owner") owner: String,
        @Path("repo") repo: String
    ): Response<List<User>>
}

課題は、コントリビューターをロードする関数のコードを変更し、これらの新しいsuspend関数を利用することである。
suspend関数はどこからも呼び出すことはできない。もし、loadContributorsBlockingから呼び出すと、「suspend関数'getOrgRepos'は、コルーチンまたは他のsuspend関数からのみ呼び出す必要があります」というエラーになる。そのため、この新しいAPIを使うためには、loadContributors関数の新しいバージョンにsuspendの印をつける必要がある。
それでは次のタスクを行っていただき、その後で、suspend関数がどのように動作するか、通常の関数とどう異なるのかを説明する。suspend関数をsuspendでない関数から呼び出す方法についても見ていく。

課題

loadContributorsBlockingの実装(src/tasks/Request1Blocking.ktに定義されている)を、loadContributorsSuspend(src/tasks/Request4Suspend.kt)にコピーすること。その後、Callを返却する関数を新しくsuspend関数を使う方法で修正すること。プログラムをSUSPENDオプションを選んで実行し、UIがGitHub要求を実行している間でも、応答することを確認する。

解法

コードの修正は簡単である。単に.getOrgReposCall(req.org).execute().getOrgRepos(req.org)に置き換え、同じ置換を二つ目の「コントリビューター」要求にも繰り返す、それだけである!その他のすべては同じである。※.execute()行は削除する。

suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .bodyList()

    return repos.flatMap { repo ->
        service.getRepoContributors(req.org, repo.name)
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}

留意すべきは、loadContributorsSuspendsuspend関数として定義されなければならない。
以前Responseを返却したexecuteを呼ぶ必要はもはやなく、なぜなら今ではAPI関数は直接responseを返却するためである。しかしそれはRetrofitライブラリ固有の実装の詳細である。他のライブラリでは、APIは異なるだろうが、考えは同じである。
suspend関数でのコードは「ブロッキング」版と驚くほど似ている。それは読みやすく、成し遂げようとしていることを正確に表している。
ブロッキング版との主な違いは、スレッドをブロックする代わりに、コルーチンをsuspendする。

block -> suspend
thread -> coroutine

コルーチンは度々軽量のスレッドと呼ばれる。これはスレッドでコードを実行する方法と同じようにコルーチンでコードを実行できるという意味である。以前ブロッキングしていた演算(そのために避けなければならなかった)は、今では代わりにコルーチンをsuspendする。
どのようにコルーチンを開始することができるか?loadContributorsSuspendを呼ぶ方法を見ると、launchの内側で呼び出していることがわかる。launchはラムダを引数にとるライブラリ関数である。

launch {
    val users = loadContributorsSuspend(req)
    updateResults(users, startTime)
}

launchは新しい計算を開始する。この計算はデータのロード処理と結果の表示を担っている。この計算は一時停止可能である。つまりネットワーク要求を行っている間、「一時停止」状態になり、配下のスレッドを解放する。ネットワーク要求が結果を応答すると、計算は復帰する。
そのような一時停止可能な計算はコルーチンと呼ばれ、このケースで簡単に言うと、launchはデータのロードと結果の表示を担う新しいコルーチンを開始する。
コルーチンはスレッドの先頭で実行し、一時停止できる計算である。コルーチンが「一時停止」状態になると、対応する計算は停止され、スレッドから取り除かれ、メモリに格納される。その間、スレッドは他の活動で埋めるために自由である。
gif
計算が継続の準備ができたら、スレッドに復帰する(ただし、必ずしも同じスレッドとは限らない)
loadContributorsSuspendの例に戻ってみよう。それぞれの「コントリビューター」要求は、今では一時停止メカニズム経由で結果を待つ。最初に新しい要求を送信する。その後、結果を待つ間、全体の「コントリビューターのロード」コルーチンは一時停止状態になる(launch関数によって開始された、先に説明したものである)。コルーチンは、対応する応答を受け取った後でのみ、復帰する。
gif2
応答を受け取るのを待つ間、スレッドは他のタスクで埋めるために自由である。COROUTINEオプション経由でユーザーをロードするときに、すべての要求がメインのUIスレッドで発生するにも関わらず、Uiが反応できる状態であるのはそのためである。

2538 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - kotlin: loaded 30 repos
2729 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - ts2kt: loaded 11 contributors
3029 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - kotlin-koans: loaded 45 contributors
...
11252 [AWT-EventQueue-0 @coroutine#1] INFO Contributors - kotlin-coroutines-workshop: loaded 1 contributors

どのコルーチンが対応するコードで実行されているか、ログで見ることができる。それを可能にするには、実行-実行構成の編集...を開き、-Dkotlinx.coroutines.debugVMオプションを追加する。
vm.png

その後、このオプションとともにmainを実行する間、コルーチンの名前はスレッド名に接続される。すべてのKotlinファイルを実行するためのテンプレートを変更することで、デフォルトでこのオプションを有効にすることもできる。
今回のケースではすべてのコードは@coroutine#1として表示される上記の「コントリビューターのロード」コルーチンという、一つのコルーチンで実行される。
このバージョンでは、結果を待つ間、他の要求を送るために、スレッドを再利用しない。なぜならコードがシーケンシャルに書かれているためである。新しい要求は、前の結果を受けたときのみ、送られる。suspend関数はスレッドを公平に扱い、「待つ」ためにブロックはしないが、並列性はまだ実現していない。これを改善する方法を見ていこう。

0
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
0
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?