序文Node.js が登場して以来、私たちがそれについて知っていることは、イベント駆動型、非ブロッキング I/O、効率的、軽量というキーワードで構成されています。これは、公式 Web サイトでも説明されているとおりです。 したがって、Nodejs に初めて触れるときには、いくつかの疑問が生じます。 1. ブラウザで実行される JavaScript が、なぜこれほど低レベルでオペレーティング システムと対話できるのでしょうか? これらの質問を見て圧倒されていると感じますか?心配しないでください。これらの質問を念頭に置いて、この記事をゆっくり読んでみましょう。 一目でわかる建築上記の質問はすべて非常に低レベルなので、Node.js 自体から始めて、Node.js の構造を見てみましょう。 Javascript で記述された Node.js 標準ライブラリは、使用時に直接呼び出すことができる API です。ソースコードの lib ディレクトリで確認できます。 ノード バインディング、このレイヤーは、Javascript と基礎となる C/C++ 間の通信の鍵となります。前者はバインディングを通じて後者を呼び出し、相互にデータを交換します。 node.ccで実装 このレイヤーは Node.js の動作をサポートする鍵であり、C/C++ で実装されています。 オペレーティングシステムとの対話たとえば、ファイルを開いて何らかの操作を実行する場合は、次のコードを記述できます。 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 がブロックされるため、単一のスレッドではユーザーが待機する必要があり、これは明らかに不合理であるため、ユーザーの要求に応答するために複数のスレッドが作成されます。 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 つだけあり、実行コンテキスト スタックを形成します。 我々が目にする 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 ソース コードの実装に対応しています。
コア関数 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 の他の関連記事に注目してください。 以下もご興味があるかもしれません:
|
<<: MySQLトリガーはPHPプロジェクトで情報のバックアップ、復元、クリアに使用されます。
>>: CentOS 7 ブートカーネルの切り替えとブートモードの切り替えの説明
これに先立ち、1日かけてやってみました。Seataは使い方が簡単で超シンプルですが、インストールや設...
1. 現在のすべての接続の詳細情報を表示します。 ./mysqladmin -uadmin -p -...
私たちウェブマスターは皆、ウェブサイトを最適化する際に記事内のキーワードを太字にすることが最適化に非...
最近のプロジェクトでフォームを作成するときに、コメント ボックスまで自動的にスクロールし、コメント ...
SPA を構築する場合、多くの場合、特定のルートを保護する必要があります。たとえば、認証されたユーザ...
【質問】 INSERT 文は最も一般的な SQL 文の 1 つです。最近、MySQL サーバーが同時...
私はしばらく MGR と連絡を取り合ってきました。MySQL 8.0.23 の登場により、MySQL...
選択して変更: クリックすると現在の値が表示され、ページ UI が表示され、CSS スタイルが変更さ...
インストール手順 rpm -ivh mysql-コミュニティ-共通-5.7.18-1.el7.x86...
SQL文 /* MySQL で重複行を削除するいくつかの方法 ---Chu Minfei ---20...
Web アプリケーションの開発とデバッグを行う際には、テストのためにブラウザのキャッシュをクリアした...
環境に関する声明ホストOS: Cetnos7.9 最小インストールdocker バージョン: 20....
例:例として、Python コード loop_hello.py を使用します。このコードは、ループ回...
目次適用シナリオ:方法 1: 正規表現 (推奨)方法2: 配列のreduceメソッドを使用する方法3...
Docker コンテナを適用する場合、多くの場合、ホスト ディレクトリを Docker コンテナにマ...