Javascript の非同期処理

記事
IT・テクノロジー

Javascript の非同期処理

Javascript を学習していて分かり難い部分に非同期処理があります。 なぜ、こんな処理をするのかプログラムを勉強したての方には分かり難いし、思ったように動かない部分です。 この記事では、その非同期処理に目を向けてみました。


非同期処理とは?

非同期処理という言葉自体が余り馴染みのない言葉で、よくわからないというのが多くの方の第一印象だと思います。 簡単に言うと、完全に正確な表現ではありませんが、プログラムを書いた順番で実行されない処理と考えると少し分かりやすくなります。

非同期の処理に対する言葉は同期処理と言いますが、これは基本的に上から順番に実行される処理です。 もちろん、プログラムには、条件で実行する部分を分けたり、繰り返し処理(ループ)などがあるので、完全に書いた順番で実行されるわけではありませんが、基本的には、処理の流れはプログラムの上から下へと言う流れで、一つ一つの処理が終わってから次の処理をするようにできているプログラミング言語が多くなっています。

ところが、Javascript では、処理が終わる前に次の処理を実行してしまうことが多く、この辺りが初心者の方には分かり難い部分になっています。この記事で紹介している、Firebase のデータベースのアクセスなども非同期の処理の代表格です。

処理には時間がかかる!

非同期の処理を行う一番の理由は、「処理には時間がかかる」からです。 当たり前の様ですが、これが非同期の処理を取り入れている大きな理由です。 特にデータを読み込んだりするのには、時間がかかります。

データをどこから読むかと言うと、いろいろあります。 主な例を挙げると:

* メモリから読む(メモリに書く)
* ディスクから読む(ディスクに書く)
* ネットワークから読む(ネットワークの機器に書く)
などです。メモリは高速にアクセスできるので、その処理時間(読み込みや書き込みの時間)は短いので、アクセスが終わるまで待っていても余り問題にならない場合がほとんどです。ところが、ディスクから読み出したりする場合は、メモリから読む場合に比べてかなり時間がかかります。最近は SSD を利用する場合が多くなっているので、かなり高速になってきましたが、メモリから読むのに比べるとまだまだ低速のアクセスです。これが、ネットワークになると場合によっては数秒間かかる場合もあって待ち時間は大きくなります。

例えば、PC のメモリから読むのにかかる時間は、数十ナノ秒(1ナノ秒= 1/1000000000 秒)です。さらに、通常は CPU にキャッシュと呼ばれるもっと早いメモリに、メモリのデータのコピーを作っておいてさらに高速でアクセスできるように工夫されています。これが、SSD のドライブの場合、通常は 100〜200 マイクロ秒(1 マイクロ秒= 1/1000000 秒)なので 1000 倍以上の時間がかかる事になります。これが、少し前まで主流だった磁気ディスク(HDD)だとデータを読み始めるまでに数十ミリ秒(1 ミリ秒= 1/1000)程度かかります。これは、ディスク上の目的のデータがある場所までヘッドを動かす必要があるからです。

このように、アクセスに時間がかかるデータが届くまで待っていると、その時間は「何も処理ができない」事になってしまいます。この待ち時間を有効に使うために考えられたのが非同期処理です。 データが届くまでの待ち時間に他の処理をしてしまおうという物です。

別の処理のやり方は、並列に処理を行う方法もあります。「プロセス」とか「スレッド」と呼ばれるものは基本的に順番に処理をする仕事の流れです。分かりやすい例に例えると、「仕事をする人」を考えてください。組み立てラインで物を組み立てる場合、必要な部品がないと組み立てをする事ができません。一人で作業する場合は、前の工程から必要な部品が来るのを待つ必要があります。その待ち時間に別の仕事をやれば、作業時間が無駄になりません。部品が届いた時点で組み立てを始めれば良いのでそれまではできる別の仕事をすると言う方法です。

会社の場合は、全体の作業の進捗が重要なので全部順番に作業をするのではなく、人を増やして並行にできる仕事は複数の人で作業をして、全部のモジュールが揃ったところで全体の組み立てを行うという事ができます。これが、複数のプロセスやスレッドを作って並列に処理をするやり方です。

Javascript の場合はシングルスレッドで実行する仕組みなので、待ち時間が長いと無駄な時間が増えてしまいます。特に Javascript は元々は Web ブラウザで実行するプログラミング言語として開発された経緯があるので、ネットワークのアクセスを行ったりする場合があるので、この待ち時間を有効に利用するために、非同期の処理を取り入れています。

なぜ非同期の処理が分かり難いのか?

通常のプログラミング言語では、処理は上から下に流れて、上の処理の結果を下の処理で利用できるので、データの流れを把握しやすくなっています。ところが非同期の処理の場合、データが届く前に下に書いてある処理を初めてしまうので、準備のできていないデータを使おうとすると、期待するデータが届いていないので思ったように動作しません。

実際にプログラムで値を表示するような処理を入れてデータを見ると、それがハッキリと分かります。

非同期で行われる処理は基本的に「時間のかかる処理」です。 例を挙げると

* ファイルの読み書き
* ネットワーク経由のアクセス
などです。

従って、プログラムを書く際に、こうした時間がかかるデータを利用する場合は、意識的にデータの準備ができてから処理をするようにプログラムを書く必要があります。それが、「コールバック関数」だったり「プロミス(promise)」などを使って明示的にデータが準備できてから処理するようにする書き方です。

コールバック関数は便利な仕組みですが、これが幾つか重なるとコードが読みにくくなるという欠点があります。 また、「await」をつけてデータが準備できるまで待つと言う書き方もできますが、これはデータが準備できるまで「待つ」と言う事なので処理に時間がかかるようになります。

Javascript は分かり難いのか?
そう考えると、Javascript は理解し難いプログラミング言語の様に感じる方もいらっしゃるかと思います。実はそういうわけでもありません。非同期の処理をしない場合、結局、処理を高速化するためには並列に処理を行う必要があります。実は、これを始めると面倒な事が起こります。並列に別々の処理しているので、「処理が終わる時間は同じではない」と言う問題が置きます。その後の処理は、必要な処理が終わってから出ないと実行できないので、処理が終わったのを確認して次の処理を始めるという管理が必要になります。この処理を「同期」と言います。一つのプロセスやスレッドだけでプログラムを書く場合は、通常は順番に処理が行われるので大きな問題にはなりません。しかし、処理を高速化しようとすると、いかに全体の仕事にかかる時間を短縮できるかになるので、待たない場合は並列処理が必要になります。 この並列に処理される仕事の「同期」は、実は Javascirpt の非同期の処理より面倒な場合が多くなります。

そう考えると、最初は少し分かり難い考え方ですが、Javascript の非同期処理はプログラムをシンプルで高速に処理するための便利な処理方法という事になります。

まとめ
Javascript の非同期処理は最初は分かり難い部分ですが、実は構想に処理を行うためにはよく考えられた方法です。 実際にデーター処理の流れを意識してプログラムを書けばそれほど難しい物ではありません。

非同期処理と上手く付き合うコツを身につけるのが、Javascript の上級プログラマーへの道と言えます。

Firebase のデータベースやストレージにアクセスは基本的に非同期で処理されるので、これらのサンプルコードをみながら実際にコードを書いている試すと非同期処理の全体像がわかってくると思います。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す