JavaScriptの非同期処理について①

今回と次回はJavaScriptの非同期処理について参考リンクなどをまとめながら書いてみます。①は主に非同期とは何なのか、その仕組みとコールバック関数による非同期処理の実行までです。

JavaScriptの非同期処理とは

プログラミングの処理には同期処理と非同期処理が存在します。同期処理とは逐次的に処理を実行する方法です。つまり処理1→処理2→処理3→・・・ というように処理を実行していく方法ということです。そのため例えば、処理2がとても時間がかかる処理だったとしてもそこで処理が止まってしまいます。時間がかかる処理とはHTTPリクエストや一定時間後に処理を実行する関数(setTimeoutなど)やファイルへのアクセス(fs.readFileなど)です。反対に非同期処理はそれらの処理をそれらの待ち時間に他の処理を実行できる方法です。JavaScriptはシングルスレッドで実行されるため同期処理でいちいち止まってしまうとよくありません。そこで、それを解決する仕組みがイベントループによる非同期処理というわけです。

マルチスレッドの問題点

マルチスレッドの問題点としてはC10K問題やスレッドセーフなどがあげられます。C10K問題とはメモリの容量によって生成可能なスレッドの上限が決まってしまいそれを超えると著しく処理速度が低下してしまう問題です。スレッドセーフとは、スレッド間でメモリを共有しているために起こる変数などの更新の競合の問題において、開発者側でスレッドを安全に保つ必要があることです。

イベントループとは

イベントループはJavaScriptの非同期処理を理解する上で凄く重要です。押さえておくべきは非同期処理は別のスレッドで実行され、最後に渡されたコールバックを(実行する)
関数スタックへ積むということです。つまり仮に一切時間がかからなくても非同期処理のコールバックは最後に実行されるということです(コード参照)。以下のサイトが分かりやすいです。

JS基礎いろいろーイベントループ

function example_async() {
    setTimeout(callback,0);
    console.log("仮にsetTimeoutが0秒でも先に実行されcallbackが最後に実行");
}
どれが非同期処理をする関数なのか

これが自分の中では少し引っかかっていました。結論は最初に述べたようにHTTPリクエストや一定時間後に処理を実行する関数(setTimeoutなど)やファイルへのアクセス(fs.readFileなど)が非同期処理となっています。非同期処理は引数にコールバックを受け取りそのコールバックは引数にerrとresultをとるような仕様が基本です。
javascriptでどれが非同期処理なのかが分かりません

コールバックを使った非同期の実装

JavaScriptの非同期プログラミングの基本であり、NodeJSのコアライブラリでも多く提供されている方法としてコールバック関数を使うやり方があります。例えば、非同期APIであるsetTimeoutはコールバック関数を引数に受け取り指定した時間後にその関数を実行してくれます。またfs.readdirも引数にコールバックを受け取ります。上でも書いた通りコールバック関数は引数にerrとresultをとるような仕様が規約となっています。以下が規約です。setTimeoutのうようにJavaScript由来のAPIなどで規約に沿っていないものも存在します。

  • コールバックがパラメータの最後であること。
  • コールバックの最初のパラメータが処理中に発生したエラー、2つ目以降のパラメータが処理の結果であること。
コールバックヘル(コールバック地獄)

上記のように非同期処理の完了後になにか処理を行いたい場合はコールバックにそれを指定すれば良いのですが、非同期処理の後にさらにその結果を使って非同期処理を行う、つまり非同期処理を逐次的に行う場合はコールバックにさらに非同期関数を指定していく必要があり、これが繰り返されるとネストが深く、可読性が低いコールバックヘル(コールバック地獄)と呼ばれる状態となります。以下のような状態です。

const fs = require('fs');

fs.readFile('data1.txt', function(data1) {
     fs.readFile('data2.txt', function(data2) {
         fs.readFile('data3.txt', function(data3) {
             fs.readFile('data4.txt', function(data4) {
                 fs.readFile('data5.txt', function(data5) {
                     console.log(data1 + data2 + data3 + data4 + data5);
                 })
             })
         })
     })
 })

このような状態になるのを回避する方法としてはES2015以降に導入されたPromise、async/awaitを使う方法があります。それを次の記事に書きたいと思います。

JavaScriptの非同期処理について② - 暇人のメモ
に続く。

参考:ハンズオン Node.js オライリー・ジャパン 2020/11/13