非同期プログラミングの一般的概念

この記事では、非同期プログラミングに関するいくつかの重要な概念と、その概念がウェブブラウザーと JavaScript でどのように見えるのかをひと通り説明していきます。このモジュールの他の記事に取り組む前に、これらの概念を理解しておく必要があります。

前提条件: 基本的なコンピューターリテラシーがあり、ある程度 JavaScript の基礎を理解していること
目的: 非同期プログラミングの基本概念と、その概念がウェブブラウザーと JavaScript でどのように表現されるかを理解すること

非同期とは?

通常は、あるプログラムのコードは書かれた順に、一度にひとつのことだけが起こるように実行されます。もしある関数が別の関数の結果に依存するのであれば、その関数は他の関数の処理が完了して結果を返すまで待たなくてはならず、それまでは、ユーザー視点からはプログラム全体は止まっているのと本質的には同じです。

例えば、Mac ユーザーは回転する虹色のカーソル(よく「ビーチボール」と呼ばれます)としてこのことを経験することもあるでしょう。このカーソルによってオペレーティングシステムは「現在使用中のプログラムは何かが終わるのを待って停止しており、それが非常に長く掛かっているので何が起こっているのかとご心配をお掛けしているのではないでしょうか」と言っているのです。

Multi-colored macOS beachball busy spinner

これはいら立つような体験であり、コンピューターの処理能力の良い使い方ではありません――特に、マルチコアプロセッサーが利用できる時代においては。他のタスクを別のプロセッサーコアに処理させて、それが終わった時に知らせることができるのに、座って待っているのは意味がありません。このように合間に別の仕事を終わらせる、ということが非同期プログラミングの基本です。非同期にタスクを実行する API は、あなたが使用するプログラミング環境(ウェブ開発であればウェブブラウザー)によって提供されます。

ブロッキングコード

非同期のテクニックは、特にウェブプログラミングにおいて非常に有用です。ウェブアプリがブラウザー上で高負荷なコードを実行すると、ブラウザーは固まって見えるかもしれません。これをブロッキングといいます。ウェブアプリがプロセッサーの制御を返すまで、ブラウザーはユーザーからの入力を処理して他のタスクを実行し続けるのを妨げられているのです。

ブロッキングが意味するところを示す例をいくつか見てみましょう。

この simple-sync.html の例(デモ参照)では、ボタンをクリックすると時間の掛かる処理(日時を 1000 万回計算し、最後に計算された日時をコンソールに出力)を実行し、DOM に段落を 1 つ追加します。

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  let myDate;
  for(let i = 0; i < 10000000; i++) {
    let date = new Date();
    myDate = date
  }

  console.log(myDate);

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

この例を実行する際は、JavaScript コンソールを開き、その後にボタンをクリックしてください ― 日時の計算が終わり、コンソールにメッセージが出力されるまで段落が現れないことに気が付くでしょう。コードは順番に実行され、前の処理が終わるまで後の処理は実行されません。

: 先ほどの例はまったく非現実的です。現実のウェブアプリでは日時を 1000 万回計算することはないでしょう! しかしながら、基本的な考え方の理解には役立ちます。

2 つ目の例の simple-sync-ui-blocking.html では(デモ参照)、実際のページでも遭遇しうる、もう少し現実的な例をシミュレーションします。ユーザーインターフェイスのレンダリングによってユーザーの操作がブロックされるのです。この例では、ボタンは 2 つあります。

  • "Fill canvas" ボタンは、クリックすると使用可能な <canvas> を 100 万個の青い円でいっぱいにします。
  • "Click me for alert" ボタンは、クリックするとアラートメッセージを表示します。
function expensiveOperation() {
  for(let i = 0; i < 1000000; i++) {
    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
    ctx.beginPath();
    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
    ctx.fill()
  }
}

fillBtn.addEventListener('click', expensiveOperation);

alertBtn.addEventListener('click', () =>
  alert('You clicked me!')
);

もし1つ目のボタンを押し、その後素早く2つ目のボタンを押すと、円の描画が終わるまでアラートが表示されないことが分かるでしょう。1つ目の処理が完了するまで 2つ目の処理がブロックされています。

: ええ、この事例は見苦しく、ブロッキングの影響を真似ているものです。しかし、これは現実のアプリケーション開発者が軽減させようと常に戦っている、ありふれた問題です。

何故こうなるのでしょうか? その答えは、一般的に言えば JavaScript はシングルスレッドだからです。ここで、スレッドの概念を紹介する必要があります。

スレッド

スレッドとは、基本的にプログラムがタスクを完了させるのに使用できる、単一のプロセスです。各スレッドは 1 度に 1 つのタスクを実行することしかできません。

Task A --> Task B --> Task C

各タスクは順次実行されます。すなわち、あるタスクが完了しなければ、その次のタスクは開始されません。

先述のように、現在では多くのコンピューターは複数のコアを持つため、一度に複数のことをすることができます。マルチスレッドをサポートするプログラミング言語は、複数のコアを使用して同時に複数のタスクを完了させることができます。

Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

JavaScript はシングルスレッド

従来より JavaScript はシングルスレッドです。複数のコアを利用しても、メインスレッドと呼ばれる単一のスレッド上でタスクを実行できるだけでしょう。これまでの例は、次のように実行されます。

Main thread: Render circles to canvas --> Display alert()

少し経ってから、JavaScript はこういった問題に役立つ、いくつかのツールを手に入れました。Web workers によって worker と呼ばれる別個のスレッドに JavaScript の処理の一部を移すことが可能となり、そのことで複数の JavaScript のコードを同時に実行することができるようになります。一般的に worker は、ユーザーの操作がブロックされないように、高コストな処理をメインスレッドとは別のところで実行するために使用されます。

  Main thread: Task A --> Task C
Worker thread: Expensive task B

このことを念頭に置いて、再び simple-sync-worker.htmlデモ参照)をブラウザーの JavaScript コンソールを開いた状態で見てみましょう。こちらは先ほどの 1000 万回の日時計算を行う例を別の worker スレッド上で行うよう書き換えたものです。今回はボタンをクリックすると、ブラウザーは日時の計算が完了する前に段落を表示することができます。1 つ目の処理は、もう 2 つ目の処理をブロックしていません。

非同期なコード

Web worker はかなり便利ですが、制限もあります。主要なものとして、Web worker は DOM にアクセスできません —— Worker に直接ユーザーインターフェイスを更新させるようなことはできません。100 万個の青い円を worker 内部で描画させることはできないのです。基本的にはただ計算ができる、ということです。

2 つ目の問題は、worker によって実行されるコードはブロックしないとは言え、根本的には依然として同期的であるということです。このことは、ある関数が先行する複数の関数の結果に頼っている場合に問題となります。次のスレッドの図について考えてみましょう。

Main thread: Task A --> Task B

このような場合、例えばタスク A は画像をサーバーから取得するような処理を、タスク B が次にその画像に対してフィルターを適用するような処理をしていると考えてみましょう。もしタスク A を実行し、その直後にタスク B を実行したとすれば、まだ画像が取得できていないためにエラーが発生するでしょう。

  Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> |      |

このような場合、例えばタスク D はタスク B と タスク C の両方の結果を利用していると考えてみましょう。もし両方の結果が同時に利用可能になることを保証できるのであれば、これで問題ないでしょうが、そのようなことはまれです。もしタスク D の入力のうち 1 つがまだ利用可能となっていない時にタスク D を実行しようとすれば、エラーが投げられるでしょう。

このような問題を解決するために、ブラウザーを利用して特定の処理を非同期に実行することができます。Promises のような機能を利用することで、ある処理(例:サーバーからの画像の取得)を実行し、その結果が返ってくるまで別の処理の実行を待たせることができるのです。

Main thread: Task A                   Task B
    Promise:      |__async operation__|

この Promise の処理はどこか別の場所で行われるため、非同期処理が実行されている間にメインスレッドがブロックされることはありません。

次の記事では、どうすれば非同期処理を書けるのかを見ていきましょう。わくわくしますよね? 読み進めましょう!

結論

現代のソフトウェアデザインは、一度に複数のことをプログラムに実行させるために、ますます非同期処理を中心とした議論が行われています。より新しく、より強力な API を利用すればするほど、非同期処理が唯一の解決先であるような事例が見つかっていくでしょう。かつては非同期なコードを書くのは困難なことでした。慣れるにはまだ時間が掛かりますが、以前よりだいぶ楽になりました。このモジュールの残りの部分では、なぜ非同期なコードが重要なのか、そして上で説明した問題を回避するコードをどのようにして設計すればよいのかを掘り下げていきます。

このモジュール内