Nodejs 探索: シングルスレッドの高並行性の原理を深く理解する

Nodejs 探索: シングルスレッドの高並行性の原理を深く理解する

序文

Node.js が登場して以来、私たちがそれについて知っていることは、イベント駆動型、非ブロッキング I/O、効率的、軽量というキーワードで構成されています。これは、公式 Web サイトでも説明されているとおりです。
Node.js® は、Chrome の V8 JavaScript エンジン上に構築された JavaScript ランタイムです。Node.js は、軽量で効率的なイベント駆動型の非ブロッキング I/O モデルを使用します。

したがって、Nodejs に初めて触れるときには、いくつかの疑問が生じます。

1. ブラウザで実行される JavaScript が、なぜこれほど低レベルでオペレーティング システムと対話できるのでしょうか?
2. Node.js は本当にシングルスレッドですか?
3. シングルスレッドの場合、大量の同時リクエストをどのように処理しますか?
4. Node.js のイベント駆動はどのように実装されていますか?

これらの質問を見て圧倒されていると感じますか?心配しないでください。これらの質問を念頭に置いて、この記事をゆっくり読んでみましょう。

一目でわかる建築

上記の質問はすべて非常に低レベルなので、Node.js 自体から始めて、Node.js の構造を見てみましょう。

Javascript で記述された Node.js 標準ライブラリは、使用時に直接呼び出すことができる API です。ソースコードの lib ディレクトリで確認できます。

ノード バインディング、このレイヤーは、Javascript と基礎となる C/C++ 間の通信の鍵となります。前者はバインディングを通じて後者を呼び出し、相互にデータを交換します。 node.ccで実装

このレイヤーは Node.js の動作をサポートする鍵であり、C/C++ で実装されています。
V8: Google がリリースした Javascript VM も、Node.js が Javascript を使用する理由の鍵です。ブラウザ以外で Javascript を実行する環境を提供します。その高い効率性が、Node.js が非常に効率的である理由の 1 つです。
Libuv: Node.js にクロスプラットフォーム、スレッド プール、イベント プール、非同期 I/O などの機能を提供し、Node.js のパワーの鍵となります。
C-ares: DNS 関連の機能を非同期的に処理する機能を提供します。
http_parser、OpenSSL、zlib など: http 解析、SSL、データ圧縮などのその他の機能を提供します。

オペレーティングシステムとの対話

たとえば、ファイルを開いて何らかの操作を実行する場合は、次のコードを記述できます。

var fs = require('fs');fs.open('./test.txt', "w", function(err, fd) { //..何かする});

このコードの呼び出しプロセスは、大まかに次のように記述できます: lib/fs.js → src/node_file.cc → uv_fs

lib/fs.js

非同期関数 open(path, flags, mode) { mode = modeNum(mode, 0o666); path = getPathFromURL(path);
  パスを検証します。
  検証Uint32(モード、'モード');
  新しいファイルハンドルを返す(
    binding.openFileHandle(pathModule.toNamespacedPath(path) を待機します。
             stringToFlags(フラグ), モード, kUsePromises));
}

src/ノードファイル.cc

static void Open(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); const int argc = args.Length(); if (req_wrap_async != nullptr) { // open(path, flags, mode, req) AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
              uv_fs_open、*パス、フラグ、モード);
  } else { // open(path, flags, mode, undefined, ctx) CHECK_EQ(argc, 5); FSReqWrapSync req_wrap_sync; FS_SYNC_TRACE_BEGIN(open); int result = SyncCall(env, args[4], &req_wrap_sync, "open",
                          uv_fs_open、*パス、フラグ、モード); FS_SYNC_TRACE_END(open);
    args.GetReturnValue().Set(結果);
  }
}

uv_fs

/* 宛先ファイルを開きます。 */
  dstfd = uv_fs_open(NULL, &fs_req,
                     req->新しいパス、
                     dst_flags、
                     statsbuf.st_mode、NULL);
  uv_fs_req_cleanup(&fs_req);

Node.js in Simple Terms からの画像:

具体的には、fs.open を呼び出すと、Node.js は process.binding を通じて C/C++ レベルの Open 関数を呼び出し、それを通じて Libuv 内の特定のメソッド uv_fs_open を呼び出します。最後に、実行結果がコールバックを通じて返され、処理が完了します。

Javascript で呼び出すメソッドは、最終的には process.binding を通じて C/C++ レベルに渡され、最終的に実際の操作が実行されます。これは、Node.js がオペレーティング システムと対話する方法です。

シングルスレッド

従来の Web サービス モデルでは、マルチスレッドは主に同時実行の問題を解決するために使用されます。I/O がブロックされるため、単一のスレッドではユーザーが待機する必要があり、これは明らかに不合理であるため、ユーザーの要求に応答するために複数のスレッドが作成されます。
http サービス用の Node.js モデル:

Node.js のシングルスレッドとは、メインスレッドが「シングルスレッド」であることを意味し、メインスレッドはコーディング順序に従ってプログラムコードをステップごとに実行します。同期コードがブロックされ、メインスレッドが占有されると、後続のプログラムコードの実行が停止します。テストコードを練習します:

var http = require('http');function sleep(time) { var _exit = Date.now() + time * 1000; while( Date.now() < _exit ) {} return;
}var server = http.createServer(function(req, res){
    睡眠(10);
    res.end('サーバーは10秒間スリープします');
});

サーバーを listen (8080);

コード ブロックのスタック図を次に示します。

まず index.js のコードをこれに変更し、ブラウザを開くと、10 秒後にブラウザが応答し、Hello Node.js と入力することがわかります。

JavaScript はインタープリタ型言語です。コードは、エンコードされて実行される順に 1 行ずつスタックにプッシュされます。実行が完了すると、コードは削除され、次のコード行が実行のためにプッシュされます。上記のコード ブロックのスタック図では、メイン スレッドが要求を受け入れると、プログラムは同期実行のためにスリープ実行ブロックにプッシュされます (これがプログラムのビジネス処理であると想定しています)。 2 番目の要求が 10 秒以内に到着すると、スタックにプッシュされ、完了するまで 10 秒間待機してから、次の要求をさらに処理します。後続の要求は一時停止され、前の同期実行が完了するまで待機してから実行されます。

すると、次のような疑問が湧いてくるかもしれません。なぜ単一のスレッドがそれほど効率的で、ブロッキングを起こさずに何万もの同時プロセスを処理できるのでしょうか?以下ではこれをイベント駆動型と呼びます。

イベント駆動/イベントループ

イベント ループは、プログラム内でイベントまたはメッセージを待機してディスパッチするプログラミング構造です。

1. 各 Node.js プロセスには、プログラム コードを実行するメイン スレッドが 1 つだけあり、実行コンテキスト スタックを形成します。
2. メインスレッドに加えて、「イベントキュー」も維持されます。ユーザーのネットワーク リクエストやその他の非同期操作が到着すると、ノードはそれをイベント キューに入れます。すぐには実行されず、コードはブロックされません。メイン スレッド コードが実行されるまで、処理は続行されます。
3. メインスレッドコードが実行されると、イベントループ、つまりイベントループ機構がイベントキューの先頭から最初のイベントを取り出し、このイベントを実行するためにスレッドプールからスレッドを割り当て、次に 2 番目のイベントを取り出し、それを実行するためにスレッドプールからスレッドを割り当て、次に 3 番目、4 番目のイベントを取り出していきます。メイン スレッドは、イベント キュー内のすべてのイベントが実行されるまで、イベント キューに未実行のイベントがあるかどうかを継続的にチェックします。その後、イベント キューに新しいイベントが追加されるたびに、メイン スレッドに通知され、イベントを順番に取り出して EventLoop に渡して処理します。イベントが実行されると、メイン スレッドに通知され、メイン スレッドはコールバックを実行し、スレッドはスレッド プールに返されます。
4. メインスレッドは上記の 3 番目のステップを繰り返します。

我々が目にする node.js のシングルスレッドは、js のメインスレッドにすぎません。非同期操作は基本的にスレッドプールによって完了します。Node は、すべてのブロッキング操作を内部スレッドプールに委託して実装します。連続ラウンドトリップスケジューリングのみを担当し、実際の I/O 操作は実行しないため、非同期の非ブロッキング I/O が実現します。これが、Node のシングルスレッドとイベント駆動の本質です。

Node.js でのイベント ループの実装:

Node.js は、js の解析エンジンとして V8 を使用し、I/O 処理には独自の libuv を使用します。Libuv は、さまざまなオペレーティング システムの基礎となる機能をカプセル化し、外部に統一された API を提供する、イベント駆動型のクロスプラットフォーム抽象化レイヤーです。イベント ループ メカニズムも実装されています。 src/node.cc 内:

環境* CreateEnvironment(IsolateData* isolate_data,
                               ローカルコンテキスト、int argc、const char* const* argv、int exec_argc、const char* const* exec_argv) {
  分離* isolate = context->GetIsolate(); HandleScope handle_scope(isolate);
  Context::Scope context_scope(context); auto env = new Environment(isolate_data, context,
                             v8_platform.GetTracingAgent());
  env->Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); env を返します。
}

このコードはノード実行環境を確立します。3 行目に uv_default_loop() がありますが、これは libuv ライブラリの関数です。uv ライブラリ自体とその中の default_loop_struct を初期化し、そのポインタ default_loop_ptr を返します。 その後、Node は実行環境をロードし、いくつかのセットアップ操作を完了してから、イベント ループを開始します。

{
    SealHandleScope シール(分離)。
    bool の詳細;
    env.performance_state()->マーク(
        ノード::パフォーマンス::NODE_PERFORMANCE_MILESTONE_LOOP_START);
    する {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks(分離);

      more = uv_loop_alive(env.event_loop()); if (more)
        続く;

      RunBeforeExit(&env); // ループが発行後または発行後にアクティブになった場合は `beforeExit` を発行します。
      // イベント後、またはいくつかのコールバックを実行した後。
      詳細は uv_loop_alive(env.event_loop()); を参照してください。
    } ながら (more == true);
    env.performance_state()->マーク(
        ノード::パフォーマンス::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
  }

  env.set_trace_sync_io(false);

  定数 int exit_code = EmitExit(&env);
  RunAtExit(&env);

more は、次のサイクルに進むかどうかを示すために使用されます。 env->event_loop() は、env に以前保存された default_loop_ptr を返し、uv_run 関数は指定された UV_RUN_DEFAULT モードで libuv のイベント ループを開始します。 I/O イベントもタイマー イベントもない場合、uv_loop_alive は false を返します。

イベントループの実行順序

Node.js の公式紹介によれば、各イベント ループには 6 つのステージが含まれており、次の図に示すように、libuv ソース コードの実装に対応しています。

  • タイマーフェーズ: このフェーズでは、タイマーのコールバック (setTimeout、setInterval) を実行します。
  • I/Oコールバックステージ: ネットワーク通信エラーコールバックなどのシステムコールエラーを実行します。
  • アイドル、準備フェーズ: ノードによって内部的にのみ使用される
  • ポーリング フェーズ: 新しい I/O イベントを取得します。適切な条件下では、ノードはここでブロックされます。
  • チェックフェーズ: setImmediate() のコールバックを実行する
  • コールバックのクローズ フェーズ: ソケットのクローズ イベント コールバックを実行します。

コア関数 uv_run: ソースコード コアソースコード

int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; //まずループがまだ実行中かどうか確認します //実行中とは、ループ内に非同期タスクがあるかどうかを意味します //実行中でない場合は、終了するだけです r = uv__loop_alive(loop); if (!r)
    uv__update_time(loop); //伝説のイベント ループです。その通りです!それはかなり長い時間です
  while (r != 0 && loop->stop_flag == 0) { //イベント更新フェーズ uv__update_time(loop); //タイマーコールバックを処理 uv__run_timers(loop); //非同期タスクコールバックを処理 ran_pending = uv__run_pending(loop); //役に立たないフェーズ uv__run_idle(loop);
    uv__run_prepare(loop); //ここで注目すべき点 //ここから次のuv__io_pollは非常に理解しにくい //まずtimeoutは時間であることを覚えておく //uv_backend_timeoutが計算された後、uv__io_pollに渡される
    //timeout = 0 の場合、uv__io_poll は直接スキップします。timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      タイムアウト = uv_backend_timeout(ループ);

    uv__io_poll(loop, timeout); // setImmediateを実行するだけ
    uv__run_check(loop); //ファイルディスクリプタとその他の操作を閉じる uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCEは前進を意味します。少なくとも1つのコールバックが
       * 返されるときに呼び出されます。uv__io_poll() は何もせずに返されることがあります。
       * タイムアウトが切れるとI/O(つまりコールバックなし)が実行されます。つまり、
       * 前進の制約を満たす保留中のタイマーがある。
       *
       * UV_RUN_NOWAITは進行状況について保証しないため、省略されます。
       * 小切手。
       */
      uv__update_time(ループ);
      uv__run_timers(ループ);
    }

    r = uv__loop_alive(loop); mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT の場合、ループが中断されます。
  } /* if文はgccに条件付きストアとしてコンパイルさせます。
   * キャッシュ ラインをダーティにする。
   */
  loop->stop_flag != 0 の場合、 loop->stop_flag = 0; r を返します。
}

コードを非常に詳細に記述したので、C コードに慣れていない人でも簡単に理解できると思います。はい、イベント ループは単なる大きな while です。こうして謎のベールが取り除かれた。

uv__io_poll ステージ

このステージは非常に巧妙に設計されています。この関数の 2 番目のパラメーターはタイムアウト パラメーターであり、このタイムアウトは uv_backend_timeout 関数から取得されます。見てみましょう。

ソースコード

int uv_backend_timeout(const uv_loop_t* loop) { if (loop->stop_flag != 0) 0 を返します。 if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) 0 を返します。 if (!QUEUE_EMPTY(&loop->idle_handles)) 0 を返します。 if (!QUEUE_EMPTY(&loop->pending_queue)) 0 を返します。 if (loop->closing_handles) 0 を返します。 return uv__next_timeout(loop);
}

これは複数ステップの if 関数であることがわかりました。1 つずつ分析してみましょう。

1. stop_flag: このフラグが0の場合、イベントループはこのラウンドの実行後に終了し、戻り時間は0であることを意味します。

2. !uv__has_active_handles と !uv__has_active_reqs: 名前が示すように、非同期タスク (タイマーと非同期 I/O を含む) がない場合、タイムアウト時間は 0 にする必要があります。

3. QUEUE_EMPTY(idle_handles) と QUEUE_EMPTY(pending_queue): 非同期タスクは pending_queue に登録されます。成功しても失敗しても登録されています。何もない場合は、この 2 つのキューは空なので、待機する必要はありません。

4. closed_handles: ループは終了段階に入ったので、待つ必要はありません。

上記のすべての条件が判断され、この文を待つために判断されます。 return uv__next_timeout(loop); この文は、uv__io_poll に「どのくらい停止しますか?」と伝えます。次に、この魔法の uv__next_timeout がどのように時間を取得するかを見ていきましょう。

int uv__next_timeout(const uv_loop_t* loop) { const struct heap_node* heap_node; const uv_timer_t* handle;
  uint64_t 差分;

  heap_node = heap_min((const struct heap*) &loop->timer_heap); if (heap_node == NULL) return -1; /* 無期限にブロックする */

  handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout time) return 0; //このコードはキーガイダンスを提供します diff = handle->timeout - loop->time; //最大値 INT_MAX を超えることはできません
  (差分>INT_MAX)の場合
    diff = INT_MAX; diffを返します。
}

待機が終了すると、チェックフェーズに入ります。その後、closing_handles フェーズに入り、イベント ループが終了します。 ソースコード解析なので詳細は省きます。公式ドキュメントを読むしかありません。

要約する

1. Nodejs はオペレーティング システムと対話します。Javascript で呼び出すメソッドは、最終的に process.binding を通じて C/C++ レベルに渡され、最終的に実際の操作を実行します。これは、Node.js がオペレーティング システムと対話する方法です。

2. いわゆるシングルスレッド Node.js はメインスレッドのみです。すべてのネットワーク要求または非同期タスクは、実装のために内部スレッド プールに引き渡されます。継続的なラウンドトリップ スケジューリングのみを担当し、イベント ループは継続的にイベント実行を駆動します。

3. Nodejs が単一のスレッドで高い並行性を処理できる理由は、libuv レイヤーのイベント ループ メカニズムと、基礎となるスレッド プールの実装によるものです。

4. イベント ループは、メイン スレッドのイベント キューからイベントを継続的に読み取り、すべての非同期コールバック関数の実行を駆動するメイン スレッドです。イベント ループには合計 7 つのステージがあり、各ステージにはタスク キューがあります。すべてのステージが 1 回ずつ順番に実行されると、イベント ループは 1 ティックを完了します。

上記は、シングルスレッドの高並行性の原理を深く理解するための Nodejs の探求の詳細な内容です。Nodejs の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • NodeJs の高メモリ使用量のトラブルシューティング実戦記録
  • Nodejs 組み込み暗号化モジュールを使用してピアツーピアの暗号化と復号化を実現する詳細な説明
  • Node.js の非同期イテレータの詳細な説明
  • Node.js組み込みモジュールの詳細な説明
  • Nodejs モジュール システムのソースコード分析
  • JS と Nodejs におけるイベント駆動型開発についての簡単な説明
  • Nodejs でモジュール fs ファイルシステムを使用する方法
  • Node.js コード実行をバイパスするためのヒントのまとめ
  • Nodejs エラー処理プロセス記録
  • Node.js を使用して C# のデータ テーブル エンティティ クラス生成ツールを作成する方法

<<:  MySQLトリガーはPHPプロジェクトで情報のバックアップ、復元、クリアに使用されます。

>>:  CentOS 7 ブートカーネルの切り替えとブートモードの切り替えの説明

推薦する

JQueryはアニメーション効果の非表示と表示を実装します

この記事では、アニメーション効果の非表示と表示を実現するためのJQueryの具体的なコードを参考まで...

docker ポートを追加して dockerfile を取得する方法

DockerイメージからDockerfileを取得する docker 履歴 --format {{....

大量のデータをMySQLにインポートする際に発生する問題と解決策の分析

プロジェクトでは、SQL を使用してデータ分析を実行するために、大量のデータをデータベースにインポー...

MySQLは、where in()順序ソートを実装するためにfind_in_set()関数を使用します。

この記事では、MySQL で find_in_set() 関数を使用して where in() の順...

Linux 上で Docker コンテナを作成、一覧表示、削除する方法の概要

1. Dockerコンテナを起動する以下のコマンドを使用して新しい Docker コンテナを起動しま...

node.jsのコアモジュールとは

目次グローバルオブジェクトグローバルオブジェクトとグローバル変数プロセスコンソール一般的なツールユー...

MySQL でストリーミングクエリを使用してデータ OOM を回避する

目次1. はじめに2. JDBCはストリーミングクエリを実装する3. パフォーマンステスト3.1. ...

CSS 水平プログレスバーと垂直プログレスバーの実装コード

時々、素敵なスクロールバー効果を見るのは楽しいものです。ここでは、CSSを使用してそれを実現する方法...

Nginx 構成 80 ポート アクセス 8080 とプロジェクト名アドレス メソッド分析

Tomcatはプロジェクトにアクセスします。通常はIP + ポート + プロジェクト名です。 Ngi...

JavaScript配列の重複排除のいくつかの方法についての詳細な説明

目次1.重複排除を設定する2. 重複を削除するには、2 回の for ループを使用します。 3. i...

jQuery で呼吸カルーセル効果を実現

この記事では、呼吸カルーセル効果を実現するためのjQueryの具体的なコードを参考までに共有します。...

vue.js ルーターのネストされたルート

序文:ルートでは、主要部分は同じでも、基礎となる構造が異なることがあります。たとえば、ホームページに...

フィボナッチ数列のJavaScript出力を実装する方法

目次トピック分析する基本的な解決策基本的な再帰再帰最適化要約するトピック私たちが答えなければならない...

MySQLにおけるMTRの概念

MTR は Mini-Transaction の略です。名前が示すように、これは「最小のトランザクシ...

HTML で余分なテキストを省略記号に変換する方法

HTML で余分なテキストを省略記号として表示したい場合は、いくつかの方法があります。 1行テキスト...