はじめに
この記事は非同期Javascriptの記事を2020年にざっと読んでみたものです。
JavaScriptを触り初めて一年が経ち非同期処理については少し理解しているものの、深くは理解していないので基本に立ち返ってしっかり読もうと思いました。
スピード重視で読むので無理な翻訳や間違いがあるかもしれません。ごめんなさい。
今日はGeneral asynchronous programming conceptsを読みます。
一般的な非同期プログラミングの考え方
この記事では非同期プログラミングに関する多くの重要な考え方について学びます。そしてその非同期プログラミングがどのようにWebブラウザやJavaScriptで表現されいるかも学びます。これらの考え方を他の記事を読む前に理解することをおすすめします。
前提知識:基本的なコンピューターリテラシー、しっかりとしたJavaScriptの基礎の理解
目的:非同期処理の背景にある基本的な考え方の理解、またどうやってそれをブラウザとJavaScriptで実現しているかの理解
非同期とは?
普通、与えられたプログラミングのコードは順々に処理が進んでいきます。このとき、ある一つのことは一回だけ起こります。もしある関数Aが他の関数Bに依存している場合、その関数Bが終了し値を返すまで関数Aは待つべきです。そしてこれら全ての処理が起こるまで、ユーザーから見ると全体のプログラムは基本的には止まっているように見えます。
例えばMacユーザーは時々、虹色のカーソル(あれビーチボールにも見えるよね)が回ったまま処理が止まっているように見えることを経験するでしょう。このカーソルは、OSが「現在、あなたが使っているプログラムは何かが完了するまで停止している必要があり、それにはとても時間がかかるため、あなたが今なにが起きているか疑問に思わないか私は心配してまっせ」と言っているようなものです。
これはイライラする経験で、そしてあまりコンピュータ処理の力をうまく利用できているとは言えません。 ______特にコンピュータが複数のプロセッサコアを使えるこの時代においては。他のタスクを他のプロセッサコアでシュッポッポと動かせ、さらにそれがいつ終わったか知らせることができるのにも関わらず、その何かが完了するまで座って待っていることはセンスがないです。この、待ってる間に他の仕事ができるようにすることこそが非同期プログラミングの基本と言えます。このようなタスクを非同期で実行できるAPIを提供するかどうかは、あなたの使っているプログラミング環境(例えばWeb開発の場合はWebブラウザ)次第です。
コード・ブロッキング(処理が止まって見える状態のこと)
非同期処理記述はとても便利です、特にwebプログラミングにおいて。あるWebアプリがあるブラウザで実行しており、ブラウザへの制御機能を保持することを無視してまでも、ある集中的なコードの塊を実行する場合、ブラウザは固まってしまったように見えるでしょう。これをブロッキングと言います。つまり、そのブラウザはユーザーの入力を処理しつづけることができなくなり、さらにそのWebアプリの処理が終わるまで他の処理を行うことができなくなります。
ここでいくつかの例で我々の言うブロッキングを示しましょう。
我々のつくったsimple-sync.htmlというファイルの例(実際にブラウザで動かしているリンクはこちら)では、クリックイベントリスナーをボタンにつけています。もしそれがクリックされたら、とある時間をめっちゃ無駄に使う処理(1000万の日付を計算し、最後の一つをコンソールに出力するもの)を行い、それが終わり次第DOMにパラグラフを挿入します。
まあコードを見てください、こんな感じです。
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のコンソール画面を開いて、そんでボタンをクリックしてください。そうしたら、日付の計算が終了しコンソールメッセージが表示されたあとに初めてパラグラフが表示されることに気づくでしょう。このコードはソースに記述された通り順番に実行され、後の処理は前の処理が完了するまで実行されていません。
注:このサンプルはあんまり現実味がないです。だって実際のwebアプリ上で1000万の日付を計算することなんてないじゃないですか。まあでも、このサンプルはあなたに基本的な考えを教えてくれます。
次のサンプル、simple-sync-ui-blocking.htmlです。(実際にブラウザで動かしているリンクはこちら。)もうちょっと現実味のあるものをシミュレートしてるので、実際のWebページをイメージできるかもしれません。ここではユーザーの入力をユーザーインターフェースを表示することによってブロックしています。このサンプルファイルには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!')
);
もし最初に"Fill canvas"ボタンをクリックしてそのあとすぐに"Click me for alert"ボタンをクリックした場合、処理中を示すサークルの表示が完了するまでアラートが表示されないことがわかります。ここでは最初の処理が2回目の処理の終了までブロックしています。
注:おっけー!我々のサンプルはちょっときもかったですね。ブロック処理を装ってみたんですけども。でも、こういうことってよくある問題なんですよ。実際のアプリ開発の現場においてはいつもこの問題を軽減できるよう頑張ってます。
なんでこんなことになってるって?一般的にJavaScriptがシングルスレッドだからです。ここで、スレッドの考え方について紹介しないといけませんね。
スレッド
スレッドとは基本的に単一の処理です。プログラムはタスクを完遂させるためにこれを使います。それぞれのタスクは一度に単一のタスクだけを行います。
タスクA --> タスクB --> タスクC
それぞれのタスクは順番に実行されます。つまり、あるタスクは次のタスクが開始される前に実行される必要があります。
さっき言ったように、多くのコンピュータは今やコアを複数持ってます。なので一度に複数のことを行うことができます。マルチスレッドをサポートしているプログラミング言語は複数のコアを、複数のタスクを同時に完遂するために使うことができます。
スレッド1: タスクA --> タスクB
スレッド2: タスクC --> タスクD
JavaScriptはシングルスレッドだよ
JavaScriptは元々はシングルスレッドです。複数のコアを持っている場合でも、シングルスレッドでタスクを実行することしかできません。このシングルスレッドをメインスレッドと言います。上記のサンプルファイルはこんな感じで実行されてます。
メインスレッド: サークルをキャンバスに表示 --> アラートを表示
その後、JavaScriptはこのようなシングルスレッドでしか処理できないという問題を解決するためにいくつかのツールを得てきました。例えば、Web workersは一つ一つ分かれたスレッドに対してJavaScriptの処理を送ることを可能にします。このスレッドをワーカー(worker)と呼びます。これによって複数のJavaScriptの処理のまとまりを同時に実行することができます。複雑な処理を行う際にメインスレッドとは別のワーカーを使うよう実装することによって、ユーザーの入力がブロックされなくなります。
メインスレッド: タスクA --> タスクC
ワーカースレッド: 複雑なタスクB
この処理を頭にイメージしてsimple-sync-worker.html(さっきとは違うよ。動いているサンプルはこちら)を見て、もう一度JavaScriptのコンソール画面を開いてみましょう。このサンプルファイルはさっきのサンプルファイルを書き直したものです。ここでは1000万の日付を独立したワーカースレッドで計算しています。今回はボタンをクリックしたときに、ブラウザはパラグラフを表示することができます。日付の計算をする前に。最初の処理はもはや次の処理をブロックしていません。
非同期処理を行うコード
Web workerはかなり便利ですが、いくつかできないことがあります。それの主要なものの一つがDOMにアクセスできないということです。つまり直接的にユーザーインターフェースを更新するためにワーカーを使うことができないということです。私たちはワーカーの処理によって100万の青い円を表示することはできない、したがって基本的には数値処理しかできないです。
二つ目の問題はワーカー内のコードはブロックされずに実行されますが、それは依然として基本的には同期的な処理であるということです。これは、ある関数が以前に起こった複数の処理の結果に依存する場合、問題となります。次のスレッドの図を見てみてください。
メインスレッド: タスクA --> タスクB
この場合において、タスクAはなにかサーバーから画像を取得するなどの処理を行い、タスクBがその後その画像に対してフィルターを適応するなどなんらかの処理を行うと考えることにしましょう。もしあなたがタスクAを開始し、そしてすぐにタスクBをを実行しようとするとエラーniなるでしょう。だってその画像はその時まだ取得できていないので。
メインスレッド: タスクA --> タスクB --> |タスク D|
ワーカースレッド: タスクC ----------> | |
この場合においては、タスクDがタスクBとCの両方の結果を利用する場合について考えています。もしこれらの結果が両方同時に利用できると保証できるのであれば大丈夫かもしれませんが、実際はそうじゃないことが多いです。もしタスクDがどちらかの入力が利用できない状態で実行される場合、それはエラーを出力することでしょう。
このような問題を解決するためにブラウザは特定の操作を非同期的に実行できるようになっています。Promises等はある実行処理(例えばサーバーから画像を取得するなと)をセットでき、セットした後、他の処理が実行される前にその結果が帰ってくるまで処理を止めることを可能とします。つまりこんな感じ。
メインスレッド: タスクA タスクB
プロミス: |___非同期処理___|
処理が別の場所で実行されているため、メインスレッドは非同期処理が行われている間ブロックされることはありません。
それでは次の記事で非同期処理を行うコードをどうやって書くのかをみていきましょう。すごいっしょ?がんばっていきましょ。
結論
現代のソフトウェアデザインは非同期プログラミングを使って動いているものが増加しており、それはプログラムに一つ以上のことを同時に行うことを可能にしている。あなたがより新しく強力なAPIを使う頃には、非同期処理を行わないとできないようなケースをいくつも確認できることでしょう。かつでは非同期処理のコードを書くのは大変でした。依然として慣れるまで時間がかかりますが、でもすごい簡単になってきています。この記事の残りでは、なぜ非同期処理のコードが重要か、またどうやって上記に書いたような問題を解決できるコードを書けるかを勉強していきます。