Skip to content

非同期プログラミング手法

何十年もの間、私たちは開発者として、アプリケーションがブロックされるのを防ぐ方法という問題に直面してきました。デスクトップ、モバイル、あるいはサーバーサイドアプリケーションを開発している場合でも、ユーザーを待たせたり、さらに悪いことにアプリケーションのスケーリングを妨げるボトルネックを引き起こしたりするのを避けたいと考えます。

この問題を解決するために、これまでに多くの方法が考案されてきました。

コルーチンがどのようなものかを説明する前に、他の解決策のいくつかについて簡単に確認しておきましょう。

スレッド

スレッドは、アプリケーションがブロックされるのを回避するための、おそらく最もよく知られたアプローチです。

kotlin
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などの一部のプラットフォームは、スレッドをサポートしていません。
  • スレッドは簡単ではない。スレッドのデバッグや競合状態の回避は、マルチスレッドプログラミングで私たちが悩まされる一般的な問題です。

コールバック

コールバックでは、ある関数を別の関数のパラメーターとして渡し、プロセスが完了したときにその関数が呼び出されるようにするという考え方です。

kotlin
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のようなイベントループアーキテクチャでは非常に一般的ですが、そこでも、一般的に人々はPromiseやReactive Extensionsのような他のアプローチを使う方向へと移行しています。

Future、Promise、その他

FutureまたはPromise(言語やプラットフォームによっては他の用語が使われることもあります)の背後にある考え方は、呼び出しを行うと、ある時点で呼び出しがPromiseオブジェクトを返すことが約束され、そのオブジェクトに対して操作を実行できるというものです。

kotlin
fun postItem(item: Item) {
    preparePostAsync() 
        .thenCompose { token -> 
            submitPostAsync(token, item)
        }
        .thenAccept { post -> 
            processPost(post)
        }
         
}

fun preparePostAsync(): Promise<Token> {
    // makes request and returns a promise that is completed later
    return promise 
}

このアプローチは、プログラミングの方法に一連の変更を要求します。具体的には、次の点が挙げられます。

  • 異なるプログラミングモデル。コールバックと同様に、プログラミングモデルはトップダウンの命令型アプローチから、チェイン呼び出しによる構成型モデルへと移行します。ループや例外処理などの従来のプログラム構造は、通常このモデルでは有効ではありません。
  • 異なるAPI。通常、thenComposethenAcceptのような全く新しいAPIを学ぶ必要があり、これらはプラットフォームによって異なることもあります。
  • 特定の戻り値の型。戻り値の型は、必要とする実際のデータから離れて、代わりに検査される必要がある新しい型Promiseを返します。
  • エラーハンドリングが複雑になる可能性がある。エラーの伝播とチェイン化は、常に単純ではありません。

Reactive Extensions

Reactive Extensions (Rx) は、Erik MeijerによってC#に導入されました。Rxは確かに.NETプラットフォームで使われていましたが、NetflixがJavaに移植し、RxJavaと名付けるまで主流にはなりませんでした。それ以来、JavaScript (RxJS) を含む様々なプラットフォーム向けに多数の移植版が提供されています。

Rxの背景にある考え方は、observable streamsと呼ばれるものへと移行することです。これにより、データがストリーム(無限の量のデータ)として考えられ、これらのストリームを監視できるようになります。実用的な観点から見ると、Rxは単にObserver パターンに、データに対する操作を可能にする一連の拡張を加えたものです。

アプローチとしてはFutureに非常に似ていますが、Futureが離散的な要素を返すのに対し、Rxはストリームを返すと考えることができます。しかし、以前のものと同様に、これもプログラミングモデルについて全く新しい考え方を導入しており、有名なフレーズで表現されています。

「すべてはストリームであり、監視可能である」

これは、問題に取り組む異なる方法を意味し、同期コードを書く際に慣れているものとはかなり大きな変化を伴います。Futureと比較して一つの利点は、非常に多くのプラットフォームに移植されているため、C#、Java、JavaScript、その他Rxが利用可能などの言語を使用しても、一般的に一貫したAPI体験を見つけることができるという点です。

さらに、Rxはエラーハンドリングに対し、いくらかより良いアプローチを導入しています。

コルーチン

Kotlinの非同期コードを扱うアプローチはコルーチンを使用することです。これはサスペンド可能な計算という考え方、つまり関数がある時点で実行を中断し、後で再開できるという考え方です。

しかし、コルーチンの利点の1つは、開発者にとってはノンブロッキングコードを書くことが、実質的にブロッキングコードを書くことと同じであるという点です。プログラミングモデル自体は実際には変わりません。

たとえば、次のコードを見てみましょう。

kotlin
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サスペンド可能関数と呼ばれるものであり、その前にsuspendキーワードがついています。これが意味することは、前述のように、関数が実行され、実行を一時停止し、ある時点で再開するということです。

  • 関数シグネチャは全く同じままです。唯一の違いはsuspendが追加されていることです。ただし、戻り値の型は、返したい型になります。
  • コードは引き続き、同期コードを書くかのように、トップダウンで書かれており、コルーチンを本質的に開始させるlaunchという関数を使用する以外の特別な構文は必要ありません(他のチュートリアルで解説されています)。
  • プログラミングモデルとAPIは同じままです。ループ、例外処理などを引き続き使用でき、全く新しいAPIセットを学ぶ必要はありません。
  • プラットフォーム非依存です。JVM、JavaScript、またはその他のプラットフォームをターゲットにしているかどうかにかかわらず、私たちが書くコードは同じです。内部では、コンパイラが各プラットフォームへの適応を処理します。

コルーチンは新しい概念ではありませんし、Kotlinによって発明されたものでもありません。何十年も前から存在し、Goのような他のプログラミング言語では人気があります。ただし重要な点は、Kotlinでの実装方法であり、その機能のほとんどはライブラリに委譲されていることです。実際、suspendキーワード以外に、言語にキーワードは追加されていません。これは、asyncawaitが構文の一部であるC#のような言語とは多少異なります。Kotlinでは、これらは単なるライブラリ関数です。

詳細については、コルーチンリファレンスを参照してください。