非同期操作の概要

シングルスレッドモデル

シングルスレッド モデルとは、JavaScript が 1 つのスレッドのみで実行されることを意味します。つまり、JavaScript は同時に 1 つのタスクのみを実行でき、他のタスクはキューに入れて待機する必要があります。

JavaScript は 1 つのスレッドでのみ実行されることに注意してください。これは、JavaScript エンジンのスレッドが 1 つだけであることを意味するわけではありません。実際、JavaScript エンジンには複数のスレッドがあります。1 つのスクリプトは 1 つのスレッド (メイン スレッドと呼ばれます) でのみ実行でき、他のスレッドはバックグラウンドで連携します。

JavaScript がマルチスレッドではなくシングルスレッドを使用する理由は、歴史と関係があります。 JavaScript は誕生以来シングルスレッドです。その理由は、マルチスレッドはリソースを共有する必要があり、Web スクリプト言語の場合はお互いの実行結果を変更する可能性があるため、ブラウザを複雑にしすぎないようにするためです。複雑。 JavaScript に同時に 2 つのスレッドがあり、1 つのスレッドが Web ページの DOM ノードにコンテンツを追加し、もう 1 つのスレッドがノードを削除する場合、ブラウザはどちらのスレッドを使用する必要がありますか?ロック機構も付いているのでしょうか?したがって、複雑さを避けるために、JavaScript は最初からシングルスレッドでした。これは言語の中核機能となっており、今後も変更されることはありません。

このモードの利点は、実装が比較的簡単で、実行環境が比較的シンプルであることです。欠点は、1 つのタスクに時間がかかると、後続のタスクをキューに入れる必要があり、全体の実行が遅れることです。プログラム。一般的なブラウザーの応答不能 (サスペンドデス) は、特定の JavaScript コードが長時間実行される (無限ループなど) ことが原因で発生することが多く、その結果、ページ全体がその場所でスタックし、他のタスクが実行できなくなります。 JavaScript 言語自体は遅くありません。遅いのは、Ajax リクエストが結果を返すのを待つなど、外部データの読み取りと書き込みです。このとき、相手のサーバーが長時間応答しなかったり、ネットワークがスムーズでなかったりすると、スクリプトが長時間停滞してしまいます。

キューの原因が大量の計算であり、CPU がビジー状態である場合は忘れてください。ただし、多くの場合、IO 操作 (入力と出力) が非常に遅いため、CPU はアイドル状態になります (ネットワークからデータを読み取る Ajax 操作など)。 ) 結果が出た後、実行を続行する必要があります。 JavaScript 言語の設計者は、現時点では、CPU が IO 操作を完全に無視し、待機中のタスクを一時停止し、後のタスクを最初に実行できることに気づきました。 IO 操作が結果を返すまで待ってから、戻って中断されたタスクの実行を続行します。この仕組みがJavaScriptの内部で使われる「イベントループ」の仕組み(Event Loop)です。

シングルスレッド モデルは JavaScript に大きな制限を課しますが、他の言語にはない利点ももたらします。うまく使えば JavaScript プログラムが詰まることがないため、Node.js は非常に少ないリソースで大量のトラフィック アクセスを処理できます。

マルチコア CPU の計算能力を活用するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御され、DOM 上で動作してはなりません。したがって、この新しい標準は JavaScript のシングルスレッドの性質を変更しません。

同期タスクと非同期タスク

プログラム内のすべてのタスクは、同期タスク (synchronous) と非同期タスク (asynchronous) の 2 つのカテゴリに分類できます。

同期タスクは、エンジンによって一時停止されず、メインスレッドで実行するためにキューに入れられるタスクです。次のタスクは、前のタスクが実行された後にのみ実行できます。

非同期タスクは、エンジンによって保留され、メインスレッドには入らずにタスクキューに入るタスクです。エンジンが非同期タスクを実行できると判断した場合 (たとえば、Ajax 操作がサーバーから結果を取得する場合) にのみ、タスクは (コールバック関数の形式で) 実行のためにメインスレッドに入ります。非同期タスクの後ろに位置するコードは、非同期タスクの終了を待たずにすぐに実行されます。つまり、非同期タスクには「ブロック」効果がありません。

たとえば、Ajax 操作は、開発者の裁量で、同期タスクまたは非同期タスクとして処理できます。同期タスクの場合、メインスレッドは Ajax オペレーションが結果を返すのを待ってから実行します。非同期タスクの場合、メインスレッドは Ajax リクエストの送信後直接実行し、結果が返されるまで待機します。 Ajax 操作の結果が返され、メインスレッドがその結果を実行します。

タスクキューとイベントループ

JavaScript の実行中、メイン スレッドの実行に加えて、エンジンはタスク キューも提供します。タスク キューには、現在のプログラムで処理する必要があるさまざまな非同期タスクが含まれます。 (実際には、タスクキューは非同期タスクの種類に応じて複数存在します。理解を容易にするため、キューは1つしか存在しないものとします。)

まず、メインスレッドがすべての同期タスクを実行します。すべての同期タスクが実行されると、タスク キュー内の非同期タスクが表示されます。条件が満たされると、非同期タスクはメインスレッドに再び入って実行を開始し、その後同期タスクになります。実行が完了すると、次の非同期タスクがメインスレッドに入り、実行を開始します。タスクキューがクリアされると、プログラムは実行を終了します。

非同期タスクは通常、コールバック関数として記述されます。非同期タスクがメインスレッドに再び入ると、対応するコールバック関数が実行されます。非同期タスクにコールバック関数がない場合、コールバック関数は次の操作を指定するために使用されないため、タスク キューには入りません。つまり、メインスレッドに再入りしません。

JavaScript エンジンは、非同期タスクに結果があるかどうか、またメインスレッドに入ることができるかどうかをどのようにして知るのでしょうか?その答えは、同期タスクが実行されている限り、エンジンは保留中の非同期タスクがメインスレッドに入ることができるかどうかを常に何度も確認するということです。このループチェックの仕組みをイベントループ(Event Loop)と呼びます。 Wikipedia は次のように定義されています。「イベント ループは、プログラム内のイベントまたはメッセージを待機し、送信するプログラミング構造です。」

非同期動作モード

以下に、非同期操作のいくつかのモードをまとめます。

コールバック関数

コールバック関数は、非同期操作の最も基本的な方法です。

以下に 2 つの関数 f1f2 を示します。プログラミングの目的は、f2 が実行される前に、f1 が実行されるまで待機する必要があることです。

関数 f1() {
  // ...
}

関数 f2() {
  // ...
}

f1();
f2();

上記のコードの問題は、「f1」が非同期操作の場合、「f2」がすぐに実行され、「f1」が終了するまで待機しないことです。

このとき、f1を書き換えて、f1のコールバック関数としてf2を書くことが考えられます。

関数 f1(コールバック) {
  // ...
  折り返し電話();
}

関数 f2() {
  // ...
}

f1(f2);

コールバック関数の利点は、シンプルで理解しやすく、実装が簡単であることですが、欠点は、コードの読み取りと保守に役立たないこと、およびさまざまな部分が高度に結合されていることです(http://en. wikipedia.org/wiki/Coupling_(computer_programming))(coupling ) により、プログラム構造が混乱し、プロセスの追跡が困難になります (特に複数のコールバック関数がネストされている場合)。また、タスクごとに指定できるコールバック関数は 1 つだけです。

イベント監視

もう 1 つのアイデアは、イベント駆動型モデルを採用することです。非同期タスクの実行は、コードの順序ではなく、イベントが発生するかどうかによって決まります。

例として「f1」と「f2」を見てみましょう。まず、f1 にイベントをバインドします (ここでは jQuery の 書き込みメソッド を使用します)。

f1.on('完了', f2);

上記のコード行の意味は、「f1」で「done」イベントが発生したら、「f2」を実行するということです。次に、f1 を書き換えます。

関数 f1() {
  setTimeout(関数() {
    // ...
    f1.trigger('完了');
  }, 1000);
}

上記のコードで、f1.trigger('done') は、実行が完了した後、すぐに done イベントがトリガーされ、f2 の実行が開始されることを意味します。

この方法の利点は、比較的理解しやすく、複数のイベントをバインドでき、各イベントが複数のコールバック関数を指定でき、「分離」できることです。 (デカップリング)、モジュール化に役立ちます。欠点は、プログラム全体をイベント駆動型にする必要があり、実行プロセスが非常に不明確になることです。コードを読んでいると、主な流れが見えにくいです。

パブリッシュ/サブスクライブ

イベントは「シグナル センター」があり、タスクが完了すると、シグナル センターにシグナルが「発行」され、シグナル センターからのシグナルを「サブスクライブ」できます。これにより、いつ実行を開始できるかを知ることができます。これは「Publish/Subscribe Pattern」(publish-subscribe パターン) と呼ばれ、「[Observer Pattern](http://en. wikipedia.org/wiki/Observer_pattern)」 (オブザーバー パターン)。

このパターンには複数の 実装 があります。以下で使用するものは、Ben Alman の Tiny Pub/Sub です。 .github.com/661855)、これは jQuery のプラグインです。

まず、「f2」はシグナル センター「jQuery」からの「done」シグナルをサブスクライブします。

jQuery.subscribe('完了', f2);

すると、「f1」は次のように書き換えられます。

関数 f1() {
  setTimeout(関数() {
    // ...
    jQuery.publish('完了');
  }, 1000);
}

上記のコードで、「jQuery.publish('done')」は、「f1」の実行が完了した後、「done」シグナルがシグナルセンター「jQuery」にリリースされ、それによって「f2」の実行がトリガーされることを意味します。 。

f2 の実行が完了したら、購読を解除できます。

jQuery.unsubscribe('完了', f2);

この方法の性質は「イベント リスニング」に似ていますが、後者よりも大幅に優れています。 「メッセージ センター」を表示してプログラムの動作を監視し、存在するシグナルの数と各シグナルの加入者数を確認できるためです。

非同期操作のプロセス制御

複数の非同期操作がある場合、非同期操作が実行される順序を決定する方法と、この順序が確実に遵守されるようにする方法というプロセス制御の問題が発生します。

関数 async(arg, callback) {
  console.log('パラメータは ' + arg +' です。結果は 1 秒後に返されます');
  setTimeout(function() { callback(arg * 2); }, 1000);
}

上記のコードの「async」関数は非同期タスクであり、コールバック関数を呼び出すまでに各実行が完了するまでに 1 秒かかります。

このような非同期タスクが 6 つある場合、最後の「final」関数を実行する前にそれらを完了する必要があります。運用プロセスはどのように整理すればよいでしょうか?

関数final() {
  console.log('完了: ', 値);
}

async(1, 関数 (値) {
  async(2, 関数 (値) {
    async(3, 関数 (値) {
      async(4, 関数 (値) {
        async(5, 関数 (値) {
          async(6、最終);
        });
      });
    });
  });
});
//パラメータは1、結果は1秒後に返されます
// パラメータは 2 で、結果は 1 秒後に返されます
//パラメータは 3 で、結果は 1 秒後に返されます
//パラメータは 4 で、結果は 1 秒後に返されます
// パラメータは 5 で、結果は 1 秒後に返されます
//パラメータは6、結果は1秒後に返されます
// 完了: 12

上記のコードでは、6 つのコールバック関数のネストは記述が面倒でエラーが発生しやすいだけでなく、保守も困難です。

シリアル実行

非同期タスクを制御するプロセス制御関数を作成できます。1 つのタスクが完了すると、別のタスクが実行されます。これをシリアル実行と呼びます。

var items = [1, 2, 3, 4, 5, 6];
var 結果 = [];

関数 async(arg, callback) {
  console.log('パラメータは ' + arg +' です。結果は 1 秒後に返されます');
  setTimeout(function() { callback(arg * 2); }, 1000);
}

関数final() {
  console.log('完了: ', 値);
}

関数シリーズ(項目) {
  if(アイテム) {
    async(アイテム, 関数(結果) {
      results.push(結果);
      シリーズを返す(items.shift());
    });
  } それ以外 {
    return Final(results[results.length - 1]);
  }
}

シリーズ(アイテム.シフト());

上記のコードでは、関数 series は非同期タスクを順番に実行する関数であり、すべてのタスクが完了した後に final 関数が実行されます。 「items」配列には各非同期タスクのパラメータが格納され、「results」配列には各非同期タスクの実行結果が格納されます。

上記の記述方法では、スクリプト全体が完了するまでに 6 秒かかります。

並列実行

プロセス制御関数は並行して実行することもできます。つまり、すべての非同期タスクが同時に実行され、すべてが完了するまで「最終」関数は実行されません。

var items = [1, 2, 3, 4, 5, 6];
var 結果 = [];

関数 async(arg, callback) {
  console.log('パラメータは ' + arg +' です。結果は 1 秒後に返されます');
  setTimeout(function() { callback(arg * 2); }, 1000);
}

関数final() {
  console.log('完了: ', 値);
}

items.forEach(関数(アイテム) {
  async(アイテム, 関数(結果){
    results.push(結果);
    if(results.length === items.length) {
      Final(results[results.length - 1]);
    }
  })
});

上記のコードでは、forEach メソッドが 6 つの非同期タスクを同時に開始し、それらがすべて完了した後、final 関数が実行されます。

それに比べて、上記の記述方法では、スクリプト全体を完了するのにわずか 1 秒しかかかりません。これは、一度に 1 つのタスクしか実行できないシリアル実行に比べて、パラレル実行の方が効率が高く、時間を節約できることを意味します。しかし、並列タスクが多いとシステムリソースが枯渇しやすく、動作速度が遅くなるという問題があります。そこで、プロセス制御には第 3 の方法があります。

パラレルとシリアルの組み合わせ

いわゆる並列処理と直列化の組み合わせでは、しきい値を設定し、一度に「n」個の非同期タスクのみを並列実行できるため、システム リソースの過剰な占有が回避されます。

var items = [1, 2, 3, 4, 5, 6];
var 結果 = [];
実行中の変数 = 0;
変数制限 = 2;

関数 async(arg, callback) {
  console.log('パラメータは ' + arg +' です。結果は 1 秒後に返されます');
  setTimeout(function() { callback(arg * 2); }, 1000);
}

関数final() {
  console.log('完了: ', 値);
}

関数ランチャー() {
  while(running < 制限 && items.length > 0) {
    var item = items.shift();
    async(アイテム, 関数(結果) {
      results.push(結果);
      実行中--;
      if(items.length > 0) {
        ランチャー();
      else if(実行中 === 0) {
        最終(結果);
      }
    });
    実行中++;
  }
}

ランチャー();

上記のコードでは、同時に実行できる非同期タスクは 2 つだけです。変数「running」は、現在実行中のタスクの数を記録します。それがしきい値よりも低い場合、新しいタスクが開始されます。これは、すべてのタスクが実行されたことを意味します。 final関数が実行されます。

このコードは、シリアル実行とパラレル実行の間にあるスクリプト全体を完了するのに 3 秒かかります。 「limit」変数を調整することで、効率とリソースの最適なバランスを実現できます。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0