Sean McQuillan
Sean McQuillan

Follow

4月30日。 2019 – 9 min read

これは、Android でコルーチンを使用することに関する複数部構成のシリーズの一部です。 この投稿では、コルーチンがどのように動作し、どのような問題を解決するかに焦点を当てます。

Other articles in this series:

Kotlin コルーチンは非同期コードを単純化するために Android で使用できる並行処理の新しい形式を紹介します。 1.3 の Kotlin では新しいものですが、コルーチンのコンセプトはプログラミング言語の黎明期からありました。 ここ数年、コルーチンの人気は高まり、今では Javascript、C#、Python、Ruby、Go など、多くの人気のあるプログラミング言語に取り入れられています。 Kotlin のコルーチンは、大規模なアプリケーションを構築するために使用されてきた確立された概念に基づいています。

Android では、コルーチンは 2 つの問題に対する素晴らしいソリューションです:

  1. Long running tasks は、メインスレッドをブロックするには長すぎるタスクのことです。

では、コルーチンがどのようにクリーンな方法でコードを構成するのに役立つか、それぞれについて見ていきましょう!

Long running tasks

Web ページの取得や API との対話はどちらもネットワーク リクエストを行うことを伴います。 同様に、データベースからの読み取りやディスクからのイメージのロードは、ファイルの読み取りを伴います。 これらの種類は、私が長時間実行タスクと呼んでいるもので、アプリが停止して待機するにはあまりにも時間がかかりすぎるタスクです!

ネットワーク要求と比較して、最新の携帯電話がコードをどれだけ速く実行するかを理解するのは難しいかもしれません。 Pixel 2 では、1 つの CPU サイクルに 0.0000000004 秒弱かかりますが、この数字は人間の目ではかなり把握しにくいものです。 しかし、ネットワークリクエストを400ミリ秒(0.4秒)程度の瞬き1回と考えれば、CPUの動作速度が理解しやすくなります。 1 回の瞬き、つまり、やや遅いネットワーク リクエストで、CPU は 10 億サイクル以上を実行できます!

Androidでは、すべてのアプリに、UI(ビューの描画など)の処理とユーザー インタラクションの調整を担当するメイン スレッドがあります。 このスレッドで発生している作業が多すぎると、アプリがハングしたり遅くなったりするように見え、望ましくないユーザー エクスペリエンスに繋がります。 アプリがフリーズしたアニメーションのような「ジャンク」と呼ばれるものを表示したり、タッチ イベントへの反応が遅くなったりしないように、長く実行するタスクはすべてメイン スレッドをブロックせずに行う必要があります。 コールバックは、将来のある時点であなたのコードにコールバックするために使用できるライブラリへのハンドルを提供します。

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

get がメイン スレッドから呼び出されたとしても、ネットワーク リクエストを実行するために別のスレッドを使用します。 そして、ネットワークから結果が得られると、メインスレッドでコールバックが呼び出される。 これは、長時間実行するタスクを処理する優れた方法で、Retrofit などのライブラリは、メイン スレッドをブロックせずにネットワーク リクエストを行うのに役立ちます。 コルーチンが長時間実行するタスクのコードをどのように単純化するかを調べるために、コルーチンを使用するように上記のコールバックの例を書き換えてみましょう。

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

このコードはメインスレッドをブロックしないのでしょうか。 ネットワーク要求を待たず、ブロックすることなく、どのように get から結果を返すのでしょうか。

コルーチンは、2 つの新しい操作を追加することにより、通常の関数に基づいて構築されます。 invoke (または call) と return に加えて、コルーチンは suspend と resume を追加する。

  • suspend – 現在のコルーチンの実行を一時停止し、すべてのローカル変数を保存する
  • resume – 一時停止したところから、中断したコルーチンを続ける

この機能は関数上の suspend キーワードにより Kotlin によって追加されています。 サスペンド関数は、他のサスペンド関数から、または launch のようなコルーチン ビルダーを使用して新しいコルーチンを開始することによってのみ呼び出すことができます。

Suspend と resume は、コールバックを置き換えるために一緒に動作します。 関数 get はメインスレッドからネットワークリクエストを実行する責任を持ったままである。 そしてネットワーク要求が完了すると、メインスレッドに通知するためにコールバックを呼び出すのではなく、単に中断したコルーチンを再開することができる。

Animation showing how Kotlin implemented suspend and resume to replace callbacks.

Looking how fetchDocs executes, you can see how suspend works.Limited は、どのように実行されるかを見ることで、サスペンドがどのように機能するかを見ることができます。 コルーチンが中断されるたびに、現在のスタックフレーム(Kotlin がどの関数が実行されているか、その変数を追跡するために使用する場所)がコピーされ、後のために保存されます。 再開すると、スタックフレームは保存された場所からコピーバックされ、再び実行を開始します。 アニメーションの途中、つまりメインスレッドのコルーチンがすべてサスペンドされると、メインスレッドは自由に画面の更新やユーザーイベントの処理ができるようになります。 サスペンドとレジュームが一緒になって、コールバックを置き換えているのです。 7509>

When the coroutines all of the main thread are suspended, the main thread is free to do other work.

When we wrote straightforward sequential code that looks exactly like a blocking network request, coroutines will run our code exactly how we want and avoid blocking the main thread.When the coroutines are all of the main thread is suspended… (参照: 『コルーチン』)

次に、メイン セーフティにコルーチンを使用する方法を見て、ディスパッチャを探ります。

Main-safety with coroutines

コルーチンでは、よく書かれたサスペンド関数は常にメインスレッドから安全に呼び出されます。 何をするにしても、どのスレッドでも常に呼び出すことができます。

しかし、Android アプリで行う多くのことは、メイン スレッドで行うには遅すぎます。 ネットワーク リクエスト、JSON の解析、データベースからの読み取りまたは書き込み、あるいは単に大きなリストに対する反復処理などです。 これらのいずれもが、ユーザーの目に見える「ジャンク」現象を引き起こすほど遅くなる可能性があり、メイン スレッドから実行されるべきです。

suspendを使用しても、バックグラウンド スレッドで関数を実行するように Kotlin に指示することはできません。 コルーチンがメインスレッドで実行されることを明確に、そして頻繁に言う価値があります。 実際、UIイベントに応答してコルーチンを起動するときに Dispatchers.Main.immediate を使用するのは本当に良いアイデアです。そうすれば、メインセーフティが必要な長時間実行タスクが終了しない場合、その結果をまさに次のフレームでユーザーに提供することができます。

Coroutine はメイン スレッドで実行され、サスペンドはバックグラウンドという意味ではありません。

メイン スレッドにとって遅すぎる作業を行う関数をメイン セーフにするには、Default または IO ディスパッチャーのいずれかで作業を行うように Kotlin Coroutines に指示することができます。 Kotlin では、すべてのコルーチンはディスパッチャで実行されなければなりません – メインスレッドで実行されているときでさえも。 コルーチンは自分自身を中断することができ、ディスパッチャはそれを再開する方法を知っているものです。

コルーチンが実行されるべき場所を指定するために、Kotlin はスレッド ディスパッチに使用できる 3 つのディスパッチャを提供します。

** Retrofit や Volley などのネットワーキング ライブラリは、独自のスレッドを管理し、Kotlin コルーチンを使用する場合、コードに明示的なメイン セーフティを要求しません。

上の例の続きとして、ディスパッチャを使って get 関数を定義してみましょう。 get の本体内部で withContext(Dispatchers.IO) を呼び出し、IO ディスパッチャー上で実行されるブロックを作成します。 そのブロックの中に入れたコードは常に IO ディスパッチャで実行されます。 withContext はそれ自体がサスペンド関数なので、メインセーフティを提供するためにコルーチンを使用して動作します。

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.Main
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main

コルーチンを使用すると、細かい制御でスレッドディスパッチを実行することができます。 withContext では、結果を返すコールバックを導入することなく、コードのどの行が実行されるかを制御できるので、データベースからの読み取りやネットワーク リクエストの実行などの非常に小さな関数に適用することができます。 そうすれば呼び出し元は、関数を実行するためにどのスレッドが必要になるかを考える必要がありません。 コルーチンはサスペンドとレジュームをサポートしているので、withContext ブロックが完了するとすぐに、メインスレッドのコルーチンが結果と共に再開されます。

よく書かれたサスペンド関数は、メインスレッド (またはメインセーフ) から呼び出して常に安全に動作します。 ディスクやネットワークに触れるようなことをする場合、 あるいは単に CPU を使いすぎる場合、withContext を使ってメインスレッドから安全に呼び出せるようにします。 これは、RetrofitやRoomのようなコルーチンベースのライブラリが追随するパターンです。 コードベース全体でこのスタイルに従えば、コードはずっとシンプルになり、スレッドに関する懸念とアプリケーションロジックが混ざり合うのを避けることができます。 一貫して従えば、コルーチンはメイン スレッドで自由に起動し、シンプルなコードでネットワークまたはデータベース要求を行うことができ、ユーザーには「ジャンク」が表示されないことが保証されます。 状況によっては、コールバックで可能なこと以上に withContext 呼び出しを最適化することも可能です。 関数がデータベースに対して10回呼び出す場合、Kotlinに10回すべての呼び出しを外側のwithContextで一度切り替えるように指示することができます。 そうすると、データベースライブラリはwithContextを繰り返し呼び出すことになりますが、同じディスパッチャにとどまり、高速なパスをたどることができます。 さらに、Dispatchers.DefaultDispatchers.IO の間の切り替えは、可能な限りスレッドスイッチを避けるように最適化されています。

次へ

この投稿では、コルーチンがどの問題を解決するのに最も適しているかを探りました。 コルーチンはプログラミング言語における非常に古い概念ですが、ネットワークと相互作用するコードをより単純化する能力により、最近人気が出てきました。

  • コードの読み書きを困難にすることなく、誤ってメイン スレッドをブロックすることがないように、正確なメイン セーフティを実行します。
  • 次回の投稿では、画面から開始したすべての作業を追跡するために Android でどのようにフィットするのかを探ります! ぜひ読んでみてください:

    コメントを残す

    メールアドレスが公開されることはありません。