非同期処理とは
現代のアプリケーション開発では非同期処理を扱う機会が数多くあるといえるでしょう。非同期処理[1]とは、処理を開始したあと、その完了を待たずに次の処理を開始するものを指します。その反対は同期処理といって処理を開始したらその完了まで一貫して行い完了するまで待つというものになります。
[1] 非同期処理については、以下の記事も併せてご覧ください。3ページ以降で非同期処理の概要や利点について分かりやすく紹介されています。
近年この非同期処理が数多く使われるようになった背景には、アプリケーションの軽快な動作を実現したいという要求が増えてきたことにあります。非同期処理を使うとどうしてアプリケーションが軽快に動くのかを理解するために、複数の機能があるモバイルアプリケーションを例に挙げて考えてみましょう。
上図のモバイルアプリケーションではニュースフィード機能があり、ニュースにはユーザーがコメントをつけることができます。そしてアプリケーションのバックグラウンドではチャット機能が動いています。ニュースへのコメント、次のニュースの受信、チャットメッセージの受信と、このモバイルアプリケーションはさまざまな一つのアプリケーションで行います。
これらの処理を同期的に、つまり一つの処理が始まってからその完了まで待つ形で行うのは効率的ではありません。以下の図は同期処理全体の流れを一つの時間軸に並べたものですが、ニュースフィード取得やチャットメッセージ取得などアプリケーション外部とのやり取りのために「待つ」ための時間帯があります。待っている状態のアプリケーション画面上では例えばスピナーが回る、あるいはフリーズするといった形でユーザーは他の操作ができなくなりユーザーにとってストレスになります。また、待ち時間の分非同期処理に比べて全ての処理が終わるまでの時間も長くかかってしまうことになります。
非同期処理の場合はアプリケーション外部とのやり取りの際、データの取得完了を待っている間にも画面上で他の操作を行うことができます。さらに下図の例でニュースフィードの取得完了前にチャットメッセージの取得を始めているように、前の処理の完了を待たずに次の処理を始められるので、複数の処理全体をより短い時間で完了させることができます。
ここまではモバイルアプリケーションのクライアント側を考えてきましたが、同時にたくさんの処理を行わなくてはならないのはクライアント側だけではありません。サーバー側では非常に多数のクライアントからの同時接続を受け付ける可能性がありますし、データベースとのやり取り、社内の他のマイクロサービスとの通信、あるいは外部のAPIと連携する必要があるかもしれません。サーバー側でも捌くことのできない処理が積み重なるとクライアントへの応答が遅れ、最悪の場合サービスの利用が一時的にできなくなってしまいます。
非同期処理がこういった場面で重要になるのは、処理全体を考えたときのスループットすなわち単位時間あたりの処理数を向上できるからです。これによってアプリケーションはクライアント側でもサーバー側でも軽快な動作を実現できます。
しかし非同期処理にはメリットばかりではありません。非同期処理を正しく組み合わせてシステム全体の処理を安全に実現することは困難さがともないます。
歴史的にさまざまな技術者や研究者が非同期処理を安全に行うための手法を提案してきました。Javaでは2004年のJava 5.0の時代にJSR-133という仕様によってJava Memory Modelが導入されました。40ページを超えるこの仕様はJavaにおける非同期処理をいかに安全に行うかについて書かれていますが、現代から見るとそこで語られるツールや概念は低レイヤーの話であり、使いこなすにはJavaのメモリに関する微細な動作の理解が要求されるものでした。
現代ではさまざまな非同期処理ツールが利用可能で、さまざまなプログラミング言語で非同期処理を行うツールが用意されています。例えばGolangではGoroutine/Channel、JavaScriptではasync/awaitおよびpromise、KotlinではCoroutineなどがあり、ここに書いた以外にもさまざまなツールが利用されています。
さらにどのツールを使うかによらず、非同期処理では最大限「イミュータブル(不変)オブジェクト」を利用するというベスト・プラクティスが広がり、これによって非同期処理のハードルが下がる面も大きかったと思います。2004年にJava Memory Modelが導入された当時、非同期処理の難しさの根源には「スレッド間共有」かつ「ミュータブル(可変)」なオブジェクトをどう安全に扱うかという問題がありました。イミュータブルオブジェクトのみを使っている限りそういった難しい問題に直面する必要はないのです。