このチュートリアルでは、非同期プログラミングへのさまざまなアプローチについて説明します
何十年もの間、開発者として、解決すべき問題、つまりアプリケーションがブロックされないようにする方法に直面してきました。デスクトップ、モバイル、さらにはサーバー側のアプリケーションを開発している場合でも、ユーザーを待たせたり、さらに悪いことに、アプリケーションのスケーリングを妨げるボトルネックを引き起こしたりすることは避けたいと考えています。
この問題を解決するには、次のような多くのアプローチがあります。
- スレッド化
- コールバック
- Futures、Promises etal。
- リアクティブエクステンション
- コルーチン
コルーチンとは何かを説明する前に、他のいくつかのソリューションを簡単に確認しましょう。
スレッド化
スレッドは、アプリケーションのブロックを回避するための最もよく知られたアプローチです。
fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
// makes a request and consequently blocks the main thread
return token
}
上記のコードで、これpreparePost
は長時間実行されるプロセスであり、その結果、ユーザーインターフェイスがブロックされると想定し ます。私たちにできることは、別のスレッドで起動することです。これにより、UIがブロックされないようにすることができます。これは非常に一般的な手法ですが、一連の欠点があります。
- スレッドは安くはありません。スレッドには、コストのかかるコンテキストスイッチが必要です。
- スレッドは無限ではありません。起動できるスレッドの数は、基盤となるオペレーティングシステムによって制限されます。サーバー側のアプリケーションでは、これが大きなボトルネックになる可能性があります。
- スレッドは常に利用できるとは限りません。JavaScriptなどの一部のプラットフォームはスレッドさえサポートしていません
- スレッドは簡単ではありません。スレッドのデバッグ、競合状態の回避は、マルチスレッドプログラミングで発生する一般的な問題です。
コールバック
コールバックでは、ある関数をパラメーターとして別の関数に渡し、プロセスが完了したらこの関数を呼び出すというアイデアがあります。
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
これは原則としてはるかに洗練された解決策のように感じますが、ここでもいくつかの問題があります。
- ネストされたコールバックの難しさ。通常、コールバックとして使用される関数は、多くの場合、独自のコールバックを必要とします。これにより、一連のネストされたコールバックが発生し、コードが理解できなくなります。このパターンは、タイトル付きのクリスマスツリーと呼ばれることがよくあります(中括弧は木の枝を表します)。
- エラー処理は複雑です。ネストモデルは、これらのエラー処理と伝播をやや複雑にします。
コールバックはJavaScriptなどのイベントループアーキテクチャで非常に一般的ですが、それでも、一般的に、人々はプロミスやリアクティブ拡張などの他のアプローチの使用に移行しています。
Futures、Promiseset。al
先物または約束の背後にある考え方(言語/プラットフォームに応じてこれらを参照できる他の用語もあります)は、電話をかけると、ある時点で約束と呼ばれるオブジェクトで戻ることが約束されているというものです。その後、操作することができます。
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
// makes request an returns a promise that is completed later
return promise
}
このアプローチでは、特にプログラミング方法に一連の変更が必要です。
- 異なるプログラミングモデル。コールバックと同様に、プログラミングモデルは、トップダウンの命令型アプローチから、連鎖呼び出しを使用する構成モデルに移行します。ループ、例外処理などの従来のプログラム構造は、通常、このモデルでは無効になります。
- さまざまなAPI。通常、
thenCompose
または などのまったく新しいAPIを学習する必要がありますがthenAccept
、これもプラットフォームによって異なる可能性があります。 - 特定の戻り値の型。戻り値の型は、必要な実際のデータから離れ、代わりに、
Promise
イントロスペクトする必要のある新しい型を返します 。 - エラー処理は複雑になる可能性があります。エラーの伝播と連鎖は必ずしも簡単ではありません。
リアクティブエクステンション
Reactive Extensions(Rx)は、Erik MeijerによってC#に導入されました 。.NETプラットフォームでは間違いなく使用されていましたが、NetflixがJavaに移植してRxJavaと名付けるまで、実際には主流の採用には至りませんでした。それ以来、JavaScript(RxJS)を含むさまざまなプラットフォーム用に多数のポートが提供されてきました。
Rxの背後にある考え方はobservable streams
、データをストリーム(無限の量のデータ)と見なし、これらのストリームを監視できる、いわゆる場所に移行 することです。実際には、Rxは 、データを操作できるようにする一連の拡張機能を備えた単なる オブザーバーパターンです。
アプローチでは、Futuresと非常に似ていますが、Futureは個別要素を返すものと考えることができ、Rxはストリームを返します。ただし、前と同様に、プログラミングモデルについてのまったく新しい考え方も導入されています。
"everything is a stream, and it's observable"
これは、問題にアプローチする別の方法と、同期コードを作成するときに使用しているものからかなり大きなシフトを意味します。Futuresとは対照的な利点の1つは、非常に多くのプラットフォームに移植されているため、C#、Java、JavaScript、またはRxが利用可能なその他の言語であるかどうかに関係なく、一般に一貫したAPIエクスペリエンスを見つけることができることです。
さらに、Rxは、エラー処理に対していくらか優れたアプローチを導入します。
コルーチン
非同期コードを操作するKotlinのアプローチは、コルーチンを使用することです。これは、一時停止可能な計算のアイデアです。つまり、関数がある時点で実行を一時停止し、後で再開できるというアイデアです。
ただし、コルーチンの利点の1つは、開発者にとって、非ブロッキングコードを作成することは、基本的にブロッキングコードを作成することと同じであるということです。プログラミングモデル自体は実際には変わりません。
たとえば、次のコードを考えてみましょう
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}
このコードは、メインスレッドをブロックすることなく、長時間実行される操作を開始します。これ preparePost
はいわゆるa suspendable function
であり、そのsuspend
前にキーワードが 付いています。上記のように、これが意味するのは、関数が実行され、実行を一時停止し、ある時点で再開するということです。
- 関数のシグネチャはまったく同じです。唯一の違いはそれ
suspend
に追加されています。ただし、戻り値の型は、返される型です。 - コードは
launch
、コルーチンを本質的に開始するという関数を使用する以外は、特別な構文を必要とせずに、トップダウンで同期コードを記述しているかのように記述されています (他のチュートリアルで説明されています)。 - プログラミングモデルとAPIは同じままです。ループや例外処理などを引き続き使用でき、新しいAPIの完全なセットを学習する必要はありません。
- プラットフォームに依存しません。JVM、JavaScript、またはその他のプラットフォームを対象としているかどうかにかかわらず、作成するコードは同じです。裏では、コンパイラが各プラットフォームへの適応を処理します。
コルーチンは、Kotlinによって発明されたのは言うまでもなく、新しい概念ではありません。それらは何十年も前から存在しており、Goなどの他のプログラミング言語で人気があります。ただし、Kotlinでの実装方法では、ほとんどの機能がライブラリに委任されていることに注意してください。実際、 suspend
キーワード以外のキーワードは言語に追加されません。これは持っているようなC#などの言語とは多少異なっている async
と await
、構文の一部として。Kotlinでは、これらは単なるライブラリ関数です。
コルーチンとさまざまな可能性の詳細については、リファレンスガイドを確認してください。