Node.jsでシングルスレッドの非ブロッキングIOモデルがどのように機能するか


325

私はノードプログラマではありませんが、シングルスレッドの非ブロッキングIOモデルがどのように機能するかに興味があります。記事理解-the-node-js-event-loopを読んだ後、私はそれについて本当に混乱しています。それはモデルの例を与えました:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que:スレッドが1つしかないため、2つの要求A(最初に来る)とBがある場合、サーバー側プログラムは最初に要求Aを処理します。SQLクエリの実行は、I / O待機を待機しているスリープステートメントです。そして、プログラムはI/O待機中にスタックし、背後にあるWebページをレンダリングするコードを実行できません。プログラムは待機中に要求Bに切り替わりますか?私の意見では、シングルスレッドモデルのため、あるリクエストを別のリクエストに切り替える方法はありません。しかし、サンプルコードのタイトルは、コードを除いてすべてが並列に実行されることを示しています。

(PSノードを使用したことがないので、コードを誤解しているかどうかわかりません。)待機中にノードをAからBに切り替える方法は?また、ノードのシングルスレッド非ブロッキングIOモデルを簡単に説明できますか?あなたが私を助けていただければ幸いです。:)

回答:


374

Node.jsは、サポートされているOS(少なくともUNIX、OS X、Windows)によって提供される非同期(非ブロッキング)入出力用のapis / syscallsを抽象化するクロスプラットフォームライブラリであるlibuvに基づいて構築されています。

非同期IO

このプログラミングモデルでは、ファイルシステムによって管理されるデバイスとリソース(ソケット、ファイルシステムなど)のオープン/読み取り/書き込み操作は、呼び出しスレッド(通常の同期cのようなモデル)をブロックせず、新しいデータまたはイベントが利用可能になったときに通知されるプロセス(カーネル/ OSレベルのデータ構造)。Webサーバーのようなアプリの場合、プロセスは、通知されたイベントがどのリクエスト/コンテキストに属しているかを把握し、そこからリクエストの処理を続行します。これは、単一のスレッドプロセスが新しいイベントを処理するためにOSがプロセスのディスパッチャーに譲る必要があったため、OSへの要求を発信したものとは異なるスタックフレームにいることを意味します。

私が説明したモデルの問題は、本質的にシーケンシャルではないため、プログラマにとって馴染みがなく、推論するのが難しいことです。「関数Aでリクエストを作成し、その結果を別の関数で処理する必要がありますが、Aのローカルは通常利用できません。」

ノードのモデル(継続渡しスタイルとイベントループ)

Nodeは、特定のプログラミングスタイルを採用するようプログラマーを誘導することにより、JavaScriptの言語機能を活用してこのモデルをもう少し同期的に見えるようにする問題に取り組みます。IOを要求するすべての関数にはのようなシグネチャがfunction (... parameters ..., callback)あり、要求された操作が完了すると呼び出されるコールバックを与える必要があります(ほとんどの時間は、OSが完了を通知するのを待つために費やされることに注意してください。他の作業に費やした)。JavaScriptのクロージャーのサポートにより、コールバックの本体内の外部(呼び出し)関数で定義した変数を使用できます。これにより、ノードランタイムによって個別に呼び出される異なる関数間で状態を保持できます。継続渡しスタイルも参照してください。

さらに、IO操作を生成する関数を呼び出した後、呼び出し側の関数は通常return、ノードのイベントループを制御します。このループは、実行がスケジュールされた次のコールバックまたは関数を呼び出します(おそらく、対応するイベントがOSから通知されたため)-これにより、複数の要求を同時に処理できます。

ノードのイベントループは、カーネルのディスパッチャにいくらか似ていると考えることができます。対応するイベントが発生したときにノードがコールバックをスケジュールしている間、カーネルは保留中のIOが完了すると、ブロックされたスレッドの実行をスケジュールします。

並行性が高く、並列処理なし

最後に、「コード以外はすべて並列で実行される」というフレーズは、ノードがすべてのjsを多重化してシーケンスすることにより、ノードが数十万のオープンソケットからのリクエストを単一のスレッドで同時に処理できるようになるという点を適切に捉えています。単一の実行ストリーム内のロジック(「すべてが並列に実行される」と言ってもおそらくここでは正しくありません- 同時実行と並列処理-違いは何ですか?)ほとんどの時間は実際にはネットワークまたはディスク(データベース/ソケット)の待機に費やされており、ロジックは実際にはCPUを集中的に使用しないため、これはWebアプリケーションサーバーではかなりうまく機能します。つまり、これはIOにバインドされたワークロードに適しています。


45
追加質問:I / Oは実際にはどのように行われますか?ノードはシステムに要求を出し、完了時に通知を要求しています。システムはI / Oを実行しているスレッドを実行していますか、それともシステムは割り込みを使用してハードウェアレベルで非同期にI / Oを実行していますか?どこかでI / Oが完了するのを待つ必要があり、それが完了するまでブロックされ、ある程度のリソースが消費されます。
— フィリップ

6
このフォローアップコメントは以下の@ user568109で回答されていることに気づきました。これら2つの回答をマージする方法があったらいいのにと思います。
— lfalin 2014

4
回答を2倍長く書いていただければ幸いです。
— Rafael Eyng、2015年

記録のために、ノードは多くの場所でサポートされています。MIPS32ルーターのファームウェアを設計していたとき、OpenWRTを介してNode.JSを実行できました。
— Qix-モニカは2015年

どのようにして、Apacheよりもスコアが高くなりますか?Apacheは、別のスレッドで同時接続を処理することもできます。
— Suhail Gupta、2016年

210

まあ、いくつかの視点を与えるために、node.jsとapacheを比較してみましょう。

ApacheはマルチスレッドのHTTPサーバーであり、サーバーが受信するすべての要求に対して、その要求を処理する個別のスレッドを作成します。

一方、Node.jsはイベント駆動型であり、すべてのリクエストを単一のスレッドから非同期で処理します。

AとBがApacheで受信されると、要求を処理する2つのスレッドが作成されます。それぞれがクエリを個別に処理し、それぞれがクエリ結果を待ってからページを提供します。ページは、クエリが完了するまで提供されます。サーバーは結果を受け取るまで残りのスレッドを実行できないため、クエリフェッチはブロックされています。

ノードでは、c.queryは非同期で処理されます。つまり、c.queryがAの結果をフェッチしている間にジャンプして、Bのc.queryを処理し、Aの結果が到着すると、結果をコールバックに送り返します。応答。Node.jsは、フェッチが終了したときにコールバックを実行することを認識しています。

私の意見では、これはシングルスレッドモデルであるため、1つの要求から別の要求に切り替える方法はありません。

実際には、ノードサーバーは常にそれを正確に実行します。スイッチを作成するには、(非同期動作)使用するほとんどの関数にコールバックがあります。

編集する

SQLクエリはmysqlライブラリから取得されます。SQLリクエストをキューに入れるためのコールバックスタイルとイベントエミッターを実装します。非ブロッキングI / Oの抽象化を提供する内部libuvスレッドによって実行されるため、非同期的には実行されません。クエリを作成するには、次の手順を実行します。

  1. dbへの接続を開きます。接続自体は非同期で行うことができます。
  2. dbが接続されると、クエリがサーバーに渡されます。クエリはキューに入れることができます。
  3. メインイベントループは、コールバックまたはイベントで完了を通知されます。
  4. メインループは、コールバック/イベントハンドラーを実行します。

httpサーバーへの着信要求は、同様の方法で処理されます。内部スレッドアーキテクチャは次のようなものです。

node.jsイベントループ

C ++スレッドは、非同期I / O(ディスクまたはネットワーク)を行うlibuvスレッドです。メインイベントループは、要求をスレッドプールにディスパッチした後も実行を続けます。待機もスリープもしないため、より多くの要求を受け入れることができます。SQLクエリ/ HTTPリクエスト/ファイルシステムの読み取りはすべてこの方法で行われます。


16
図は非常に便利です。
— Anmol Saraf 2014年

14
待ってください。ダイアグラムには「内部C ++スレッドプール」があります。これは、すべてのIOブロッキング操作がスレッドを生成することを意味しますよね?つまり、私のノードアプリがすべてのリクエストに対してIOを機能する場合、ノードモデルとApacheモデルの間に実質的な違いはありませんか?この部分は申し訳ありません。
— gav.newalkar 2015年

21
@ gav.newalkarスレッドを生成せず、リクエストはキューに入れられます。スレッドプール内のスレッドはそれらを処理します。スレッドは動的ではなく、Apacheのようにリクエストごとです。それらは通常固定されており、システムごとに異なります。
— user568109 2015年

10
@ user568109しかし、Apacheもスレッドプールを使用しています(httpd.apache.org/docs/2.4/mod/worker.html)。つまり、結局のところ、node.jsを使用した設定の違いは、Apacheを前面に配置した場合とスレッドプールが配置されている場所だけの違いではないでしょうか。
— クリス

13
その図は公式ドキュメントの最初のページにあるはずです。
— bouvierr 2016年

52

Node.jsは舞台裏でlibuvを使用します。libuv にはスレッドプール(デフォルトではサイズ4)があります。したがって、Node.js はスレッドを使用して並行性を実現します。

ただし、コードは単一のスレッドで実行されます(つまり、Node.js関数のすべてのコールバックが同じスレッドで呼び出されます(いわゆるループスレッドまたはイベントループ)。「Node.jsがシングルスレッドで実行される」と言うと、実際には「Node.jsのコールバックはシングルスレッドで実行される」と言っています。


1
短いが明確な答え(y)
— Sudhanshu Gaur 2017

1
良い答えI / Oはこのメインのイベントループ、ループスレッド、リクエストスレッドの外で行われることを追加します
— Ionut Popa

それが私が2時間から検索していた答えであり、シングルスレッドアプリケーションで同時実行性がどのように管理されたか
— Muhammad Ramzan

うん、「次のレベル」の答えを得るのは難しい。これは、IOが実際に行われる場所(スレッドプール内のどこか)を説明します
— Oliver Shaw

9

Node.jsは、イベントループプログラミングモデルに基づいています。イベントループはシングルスレッドで実行され、繰り返しイベントを待機してから、それらのイベントにサブスクライブしているイベントハンドラーを実行します。イベントは、例えば

  • タイマー待機が完了しました
  • データの次のチャンクは、このファイルに書き込む準備ができています
  • 新しいHTTPリクエストがやって来る

これらはすべてシングルスレッドで実行され、JavaScriptコードが並列で実行されることはありません。これらのイベントハンドラーが小さく、さらに多くのイベント自体を待つ限り、すべてがうまく機能します。これにより、複数のリクエストを単一のNode.jsプロセスで同時に処理できます。

(イベントが発生する場所として、内部には少し魔法があります。その一部には、並列で実行される低レベルのワーカースレッドが含まれます。)

このSQLの場合、データベースクエリを作成してからその結果をコールバックで取得するまでの間、多くのことが(イベント)発生します。その間、イベントループはアプリケーションに生命を送り続け、一度に1つの小さなイベントを他のリクエストに送り続けます。したがって、複数のリクエストが同時に処理されます。

イベントループの高レベルビュー

:によると、「 - Node.jsの背後にあるコアコンセプト万フィートからのイベントループ」。


5

関数c.query()には2つの引数があります

c.query("Fetch Data", "Post-Processing of Data")

この場合の「データのフェッチ」操作はDB-Queryですが、ワーカースレッドを生成してDB-Queryを実行するこのタスクを実行することにより、Node.jsで処理できるようになりました。(Node.jsは内部でスレッドを作成できることに注意してください)。これにより、関数は遅延なく瞬時に戻ることができます

2番目の引数「データの後処理」はコールバック関数です。ノードフレームワークはこのコールバックを登録し、イベントループによって呼び出されます。

したがって、ステートメントc.query (paramenter1, parameter2)は瞬時に戻り、ノードが別の要求に応えることができます。

PS:私はノードを理解し始めたばかりですが、実際にはこれを@Philipへのコメントとして書きたかったのです が、十分な評判ポイントがなかったため、回答として書きました。


3

もう少し読んだ場合-「もちろん、バックエンドには、DBアクセスとプロセス実行のためのスレッドとプロセスがあります。ただし、これらは明示的にコードに公開されていないため、知る以外に心配する必要はありません。これらのスレッドからの結果がイベントループを介してコードに返されるため、データベースや他のプロセスとのI / O対話は、各要求の観点から非同期になります。」

about-"コード以外はすべて並列で実行されます"-コードは同期的に実行されます。IOの待機などの非同期操作を呼び出すと、イベントループがすべてを処理してコールバックを呼び出します。それはあなたが考えなければならないものではありません。

あなたの例では:2つのリクエストA(最初に来る)とBがあります。リクエストAを実行し、コードは引き続き同期的に実行され、リクエストBを実行します。イベントループはリクエストAを処理し、終了するとリクエストAのコールバックを呼び出しますその結果、リクエストBも同様です。


3
「もちろん、バックエンドには、DBアクセスとプロセス実行のためのスレッドとプロセスがあります。ただし、これらは明示的にコードに公開されていません」 -このフレーズから取った場合、どのノードにも違いはありません。行うか、またはマルチスレッドフレームワーク(JavaのSpring Frameworkとしましょう)が行います。スレッドはありますが、作成を制御することはできません。
— Rafael Eyng

@RafaelEyng複数のリクエストのシリーズを処理するために、ノードは常にそのための単一のスレッドを持っていると思います。各コールバックがdbアクセスのような他のプロセスとは別にスレッドの新しいインスタンスに配置されるかどうかはわかりませんが、少なくともノードは、処理前にインラインで待機する必要がある要求を受信するたびにスレッドをインスタンス化しないことが確実にわかります(実行前に実行)コールバック)。
— コールドケルベロス

1

さて、これまでのところほとんどのことははっきりしているはずです... トリッキーな部分はSQLです。それが実際に別のスレッドまたはプロセスで実行されていない場合、SQL実行は個々のステップに( SQLの非ブロックのものが実行される非同期実行のために作られたプロセッサ!)、およびブロッキングのもの(例えば睡眠)は、実際にすることができます()割り込み/イベントアラームとしてカーネルに移してのイベントリストに入れられますメインループ。

つまり、たとえば、SQLなどの解釈はすぐに行われますが、待機中(カーネルによって将来的に発生するイベントとして、いくつかのkqueue、epoll、...構造、他のIO操作と一緒に格納されます) )メインループは他のことを行うことができ、最終的にそれらのIOで何かが起こったかどうかを確認して待機します。

したがって、もう一度言い換えると、プログラムがスタックすることは決してありません(取得が許可されています)。スリープ状態の呼び出しは決して実行されません。彼らの義務は、カーネル(何かを書く、何かがネットワーク上に来るのを待つ、時間が経過するのを待つ)または別のスレッドまたはプロセスによって行われます。–ノードプロセスは、各イベントループサイクルでOSへの唯一のブロッキングコールにおいて、これらの義務の少なくとも1つがカーネルによって完了しているかどうかをチェックします。すべての非ブロッキングが行われると、その時点に到達します。

晴れ?:-)

私はノードを知りません。しかし、c.queryはどこから来たのですか?


kqueue epollは、Linuxカーネルでのスケーラブルな非同期I / O通知用です。ノードにはそのためのlibuvがあります。ノードは完全にユーザーランドにあります。カーネルの実装には依存しません。
— -user568109

1
@ user568109、libuvはNodeの仲介者です。非同期フレームワークは、カーネルの非同期I / Oサポートに(直接かどうかにかかわらず)依存します。そう?
— Robert Siemer 2013年

混乱させて申し訳ありません。ソケット操作には、カーネルからの非ブロックI / Oが必要です。非同期処理を行います。ただし、非同期ファイルI / Oはlibuv自体によって処理されます。あなたの答えはそれを教えてくれません。どちらも同じように扱われ、カーネルによって処理されます。
— user568109 2013年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.