ソース記事はこちら
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()
}
留意すべきは、loadContributorsSuspend
はsuspend
関数として定義されなければならない。
以前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
はデータのロードと結果の表示を担う新しいコルーチンを開始する。
コルーチンはスレッドの先頭で実行し、一時停止できる計算である。コルーチンが「一時停止」状態になると、対応する計算は停止され、スレッドから取り除かれ、メモリに格納される。その間、スレッドは他の活動で埋めるために自由である。
計算が継続の準備ができたら、スレッドに復帰する(ただし、必ずしも同じスレッドとは限らない)
loadContributorsSuspend
の例に戻ってみよう。それぞれの「コントリビューター」要求は、今では一時停止メカニズム経由で結果を待つ。最初に新しい要求を送信する。その後、結果を待つ間、全体の「コントリビューターのロード」コルーチンは一時停止状態になる(launch
関数によって開始された、先に説明したものである)。コルーチンは、対応する応答を受け取った後でのみ、復帰する。
応答を受け取るのを待つ間、スレッドは他のタスクで埋めるために自由である。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.debug
VMオプションを追加する。
その後、このオプションとともにmain
を実行する間、コルーチンの名前はスレッド名に接続される。すべてのKotlinファイルを実行するためのテンプレートを変更することで、デフォルトでこのオプションを有効にすることもできる。
今回のケースではすべてのコードは@coroutine#1
として表示される上記の「コントリビューターのロード」コルーチンという、一つのコルーチンで実行される。
このバージョンでは、結果を待つ間、他の要求を送るために、スレッドを再利用しない。なぜならコードがシーケンシャルに書かれているためである。新しい要求は、前の結果を受けたときのみ、送られる。suspend
関数はスレッドを公平に扱い、「待つ」ためにブロックはしないが、並列性はまだ実現していない。これを改善する方法を見ていこう。