それでも内部的にスレッドに依存している場合、Node.jsは本質的にどのように速くなりますか?


281

私は次のビデオを見ました。Node.jsの概要ですが、速度の利点をどのように得るかがまだわかりません。

主に、ある時点でRyan Dahl(Node.jsの作成者)は、Node.jsはスレッドベースではなくイベントループベースであると述べています。スレッドは高価であり、利用するコンカレントプログラミングの専門家にのみ任せるべきです。

その後、彼は、独自のスレッドプールを内部に持つ、基になるC実装を持つNode.jsのアーキテクチャスタックを示します。したがって、Node.js開発者は自分のスレッドを開始したり、スレッドプールを直接使用したりすることはありません。非同期コールバックを使用します。それだけ私は理解しています。

私が理解していないのは、Node.jsがまだスレッドを使用しているという点です...これは実装を非表示にするだけなので、50人が50個のファイル(現在メモリ内にない)を要求すると、50スレッドは必要ない場合、これはどのように速くなりますか?

唯一の違いは、Node.js開発者は内部で管理されているため、スレッド化された詳細をコーディングする必要はないが、その下では依然としてスレッドを使用してIO(ブロッキング)ファイル要求を処理していることです。

それで、あなたは本当に1つの問題(スレッド)を取り、その問題がまだ存在している間それを隠すことではないですか?主に複数のスレッド、コンテキスト切り替え、デッドロック...など?

ここにはまだ理解していない詳細があるはずです。


14
私はあなたがその主張が幾分過度に単純化されていることに同意する傾向があります。ノードのパフォーマンス上の利点は、次の2つに要約できます。1)実際のスレッドはすべてかなり低いレベルで含まれているため、サイズと数に制約があり、スレッドの同期が単純化されます。2)OSレベルの「切り替え」select()は、スレッドコンテキストスワップよりも高速です。
先のとがった

回答:


140

ここでは、いくつかの異なることが混同されています。しかし、それはスレッドが本当に難しいというミームから始まります。したがって、スレッドが難しい場合は、スレッドを使用して1)バグが原因で中断し、2)スレッドをできるだけ効率的に使用しない可能性が高くなります。(2)はあなたが尋ねているものです。

彼が提供する例の1つについて考えてみましょう。ここで、リクエストが来てクエリを実行し、その結果を使って何かを行います。標準の手順で記述した場合、コードは次のようになります。

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

受け取ったリクエストによって上記のコードを実行する新しいスレッドが作成された場合は、そこにスレッドが存在し、query()実行中は何もしません。(Ryanによると、Apacheは元の要求を満たすために単一のスレッドを使用していますが、nginxは、そうでないために彼が話しているケースではそれよりも優れています。)

さて、もしあなたが本当に賢いのであれば、クエリを実行している間に環境が停止して別のことを実行できるような方法で上記のコードを表現するでしょう:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

これは基本的にnode.jsが行っていることです。あなたは基本的に、言語と環境のために便利な方法で、したがってクロージャについてのポイントである装飾を行っています-環境が何をいつ実行するかについて環境が賢くできるような方法でコードを装飾します。このように、node.jsは非同期I / Oを発明したという意味では新しいものではありませんが(誰かがこのようなものを主張したわけではありません)、表現方法が少し異なるという点で新しいものです。

注:私が言うには、実行する環境と時間について環境が賢明である可能性があります。具体的には、I / Oの開始に使用されていたスレッドを使用して、他の要求や実行可能な計算を処理できるようになります。並列、または他の並列I / Oを開始します。(同じリクエストに対してより多くの作業を開始できるほど洗練されたノードであるとは限りませんが、そのアイデアは理解できます。)


6
さて、これがパフォーマンスを向上させる方法が確実にわかります。IOが返ってくるのを待っているスレッドや実行スタックがなく、ライアンが行ったことを効果的に見つけることができるので、CPUを最大限に活用できるように聞こえるからです。すべてのギャップを埋める方法。
Ralph Caraveo

34
ええ、私が言えるのは、彼がギャップを埋める方法を見つけたわけではないということです。それは新しいパターンではありません。違いは、彼がJavascriptを使用して、プログラマーにこの種の非同期に対してはるかに便利な方法でプログラムを表現させていることです。ひどく細かいディティールかもしれませんが、それでも...
jrtipton

16
多くのI / Oタスクで、Nodeは利用可能なカーネルレベルの非同期I / O API(epoll、kqueue、/ dev / pollなど)を使用することも指摘する価値があります
Paul

7
私はそれを完全に理解しているかどうかまだわかりません。Webリクエスト内でIOオペレーションがリクエストの処理に必要な時間のほとんどを占めるものであると考えた場合、各IOオペレーションに対して新しいスレッドが作成されると、非常に高速に発生する50のリクエストに対して、おそらく50個のスレッドが並行して実行され、それらのIO部分を実行しています。標準のWebサーバーとの違いは、node.jsではそのIO部分のみですが、そこではリクエスト全体がスレッドで実行されるということですが、それはほとんどの時間を費やしてスレッドを待機させる部分です。
Florin Dumitrescu 2013

13
@SystemParadox指摘してくれてありがとう。私は実際に最近このトピックについていくつかの調査を行いましたが、実際の問題は、非同期I / Oがカーネルレベルで適切に実装されている場合、非同期I / O操作の実行中にスレッドを使用しないことです。代わりに、I / O操作が開始されるとすぐに呼び出し側のスレッドが解放され、I / O操作が完了してスレッドが使用可能になるとコールバックが実行されます。そのため、I / O操作の非同期サポートが適切に実装されている場合、node.jsは、1つのスレッドのみを使用して、50のI / O操作で50の同時リクエストを(ほぼ)並列で実行できます。
Florin Dumitrescu 2013年

32

注意!これは古い答えです。大まかな概要ではまだ正しいですが、ここ数年のNode.jsの急速な発展により、一部の詳細が変更された可能性があります。

次の理由でスレッドを使用しています。

  1. open()O_NONBLOCKオプションは、ファイルでは機能しません
  2. 非ブロッキングIOを提供しないサードパーティライブラリがあります。

非ブロックIOを偽造するには、スレッドが必要です。別のスレッドでブロックIOを実行してください。これは醜いソリューションであり、多くのオーバーヘッドを引き起こします。

ハードウェアレベルではさらに悪いです。

  • DMA CPUは、非同期IOをオフロードします。
  • データは、IOデバイスとメモリの間で直接転送されます。
  • カーネルはこれを同期的なブロックシステムコールでラップします。
  • Node.jsは、ブロッキングシステムコールをスレッドにラップします。

これは単なる愚かで非効率的です。しかし、少なくともそれは機能します!Node.jsは、イベント駆動の非同期アーキテクチャーの背後にある醜くて扱いにくい詳細を隠すため、楽しむことができます。

多分誰かが将来ファイルにO_NONBLOCKを実装するでしょうか?...

編集:私は友人とこれについて話しました、そして彼はスレッドの代替がselectでポーリングしていると私に言いました:0のタイムアウトを指定し、返されたファイル記述子でIOを実行します(今、それらはブロックされないことが保証されています)。


Windowsはどうですか?
パセリエ2017

すみません、わかりません。libuvが非同期作業を行うためのプラットフォーム中立層であることだけを知っています。Nodeの初めにはlibuvはありませんでした。その後、libuvを分割することが決定され、プラットフォーム固有のコードがより簡単になりました。言い換えれば、Windowsには独自の非同期のストーリーがあり、Linuxとは完全に異なる可能性がありますが、私たちにとってはlibuvが私たちのためにハードワークを行うので、それは問題ではありません。
nalply

28

ここで「間違ったことをしている」のではないかと心配です。特に、一部の人々が作成したきちんとした小さな注釈をどのように作成するかはわかりません。しかし、私はこのスレッドで行う多くの懸念/観察があります。

1)人気のある回答の1つにある疑似コードのコメント要素

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

本質的に偽物です。スレッドが計算している場合、それは親指をいじるのではなく、必要な作業を行っています。一方、単にIOの完了を待機しているだけで、CPU時間を使用していない場合、カーネル内のスレッド制御インフラストラクチャの要点は、CPUが何か便利なことを見つけるということです。ここで提案されている「親指をいじる」唯一の方法は、ポーリングループを作成することです。実際のWebサーバーをコーディングした人は、それを行うのに十分な能力を備えていません。

2)「スレッドは難しい」。データ共有のコンテキストでのみ意味があります。独立したWebリクエストを処理する場合のように、本質的に独立したスレッドがある場合、スレッド化は非常に簡単です。1つのジョブを処理する方法の線形フローをコード化し、複数のリクエストを処理することを理解し、実質的に独立しています。個人的には、ほとんどのプログラマーにとって、クロージャー/コールバックのメカニズムを学ぶことは、単に上から下へのスレッドバージョンをコーディングするよりも複雑であることを知っています。(ただし、スレッド間で通信する必要がある場合、人生は本当に非常に速くなりますが、クロージャー/コールバックのメカニズムが実際にそれを変更するとは確信していません。このアプローチはまだスレッドで実現可能であるため、オプションを制限するだけです。とにかく、それは

3)これまでのところ、特定のタイプのコンテキストスイッチが他のタイプのコンテキストスイッチよりも時間がかかる理由について、誰も実際の証拠を提示していません。マルチタスクカーネル(組み込みコントローラーの小規模なもので、「実際の」OSほど豪華ではない)を作成した経験から、これは当てはまらないことが示唆されています。

4)私がこれまでに見た、他のウェブサーバーよりもノードがどれだけ速いかを示すことを意図しているすべてのイラストには、ひどい欠陥がありますが、ノードに確実に受け入れられる1つの利点を間接的に示す方法で欠陥があります(そしてそれは決して重要ではありません)。ノードは、チューニングが必要な(実際には許可さえされていない)ように見えません。スレッドモデルがある場合は、予想される負荷を処理するのに十分なスレッドを作成する必要があります。これをひどく行うと、パフォーマンスが低下します。スレッドが少なすぎる場合、CPUはアイドル状態ですが、追加の要求を受け入れることができず、作成するスレッドが多すぎるため、カーネルメモリが浪費され、Java環境の場合は、メインヒープメモリも無駄になります。 。さて、Javaにとって、ヒープの浪費は、システムのパフォーマンスを台無しにする最初の、最良の方法です。効率的なガベージコレクション(現在、これはG1で変更される可能性がありますが、陪審は少なくとも2013年の初めの時点ではまだその点で出ていないようです)は、多くの予備ヒープがあるかどうかに依存するためです。したがって、問題があります。スレッドが少なすぎるとチューニングが行われ、CPUがアイドル状態になりスループットが低下し、多すぎるとチューニングが失敗し、他の点で問題が発生します。

5)Nodeのアプローチは「設計上、より高速である」という主張の論理を受け入れる別の方法があります。それがこれです。ほとんどのスレッドモデルは、タイムスライスされたコンテキストスイッチモデルを使用し、より適切な(値判断アラート:)およびより効率的な(値判断ではない)プリエンプティブモデルの上に階層化されます。これは2つの理由で発生します。1つ目は、ほとんどのプログラマーが優先プリエンプションを理解していないように見えることです。 ;特に、Javaの最初のバージョンでは、Solaris実装で優先度の優先使用とWindowsでのタイムスライスが使用されていました。ほとんどのプログラマーは、「Solarisではスレッドが機能しない」ことを理解していないため、彼らはモデルをどこでもタイムスライスに変更しました)。とにかく、要点は、タイムスライシングが追加の(そして潜在的に不要な)コンテキストスイッチを作成することです。すべてのコンテキストスイッチはCPU時間を消費し、その時間は実際の手元のジョブで実行できる作業から効果的に削除されます。ただし、かなり奇妙なことが起こっている場合を除いて、タイムスライシングのためにコンテキストの切り替えに費やされる時間は、全体の時間のごくわずかな割合を超えてはなりません。シンプルなウェブサーバー)。したがって、はい、タイムスライシングに伴う過剰なコンテキストスイッチは非効率的です(そして、これらは その時間は、手元にある実際の作業で実行できる作業から効果的に取り除かれます。ただし、かなり奇妙なことが起こっている場合を除いて、タイムスライシングのためにコンテキストの切り替えに費やされる時間は、全体の時間のごくわずかな割合を超えてはなりません。シンプルなウェブサーバー)。したがって、はい、タイムスライシングに伴う過剰なコンテキストスイッチは非効率的です(そして、これらは その時間は、手元にある実際の作業で実行できる作業から効果的に取り除かれます。ただし、かなり奇妙なことが起こっている場合を除いて、タイムスライシングのためにコンテキストの切り替えに費やされる時間は、全体の時間のごくわずかな割合を超えてはなりません。シンプルなウェブサーバー)。したがって、はい、タイムスライシングに伴う過剰なコンテキストスイッチは非効率的です(そして、これらは原則としてカーネルスレッド)ですが、違いはスループットの数パーセントであり、Nodeによく含まれるパフォーマンスの主張に含まれる整数の要素ではありません。

とにかく、そのすべてが長くて乱暴なものであることをお詫びしますが、私は今のところ、議論は何も証明しておらず、これらの状況のいずれかで誰かから連絡をいただければ幸いです:

a)Nodeの方が優れている理由の実際の説明(上記で概説した2つのシナリオを超えて、その最初の(微調整)は、これまでに見たすべてのテストの実際の説明だと思います。([編集]実際、考えれば考えるほど、膨大な数のスタックで使用されるメモリがここで重要になるのではないかと考えています。最近のスレッドのデフォルトのスタックサイズはかなり大きくなる傾向がありますが、クロージャベースのイベントシステムは必要なものだけです)

b)選択したスレッド化サーバーに実際に公平な機会を与える実際のベンチマーク。少なくともその方法では、主張が本質的に偽であると信じるのをやめる必要があります;>([編集]これはおそらく私が意図したものよりも強力ですが、パフォーマンスの利点についての説明はせいぜい不完全であり、示されているベンチマークは不合理です)。

乾杯、トビー


2
スレッドの問題:RAMが必要です。非常にビジーなサーバーでは、最大数千のスレッドを実行できます。Node.jsはスレッドを回避するため、より効率的です。効率はコードをより速く実行することによるものではありません。コードがスレッドで実行されるか、イベントループで実行されるかは問題ではありません。CPUについても同じです。しかし、スレッドを廃止することで、RAMを節約します。数千のスタックではなく、1つのスタックのみです。また、コンテキストスイッチも保存します。
nalply

3
しかし、ノードはスレッドを廃止していません。それでも、ほとんどのWeb要求に必要なIOタスクに内部的にそれらを使用します。
levi 2014年

1
また、ノードはコールバックのクロージャーをRAMに保存するため、どこで勝ったかわかりません。
Oleksandr Papchenko

@leviしかし、nodejsは「リクエストごとに1つのスレッド」のようなものを使用しません。おそらく、非同期IO APIの使用による複雑さを回避するために、IOスレッドプールを使用します(POSIX open()を非ブロッキングにすることはできませんか?)。このようにして、従来のfork()/ pthread_create()-on-requestモデルがスレッドを作成および破棄しなければならない場合のパフォーマンスヒットを償却します。そして、追記a)で述べたように、これはスタックスペースの問題も償還します。たとえば、16のIOスレッドで十分に何千ものリクエストを処理できます。
binki

「最近のスレッドのデフォルトのスタックサイズはかなり大きくなる傾向がありますが、クロージャベースのイベントシステムによって割り当てられるメモリは必要なものだけです」これらは同じ順序である必要があるという印象を受けます。クロージャは安価ではありません。ランタイムはシングルスレッドアプリケーションのコールツリー全体をメモリ内に保持し(いわば「スタックをエミュレート」)、ツリーのリーフが関連するクロージャとして解放されたときにクリーンアップできるようにする必要があります。 「解決」されます。これには、ガベージコレクションできないヒープ上のものへの多くの参照が含まれ、クリーンアップ時にパフォーマンスに影響します。
David Tonhofer、2016年

14

私が理解していないのは、Node.jsがまだスレッドを使用しているという点です。

一部のパーツは非ブロッキングを書き込むのが非常に難しいので、Ryanはブロッキングしているパーツにスレッドを使用します(ほとんどのnode.jsは非ブロッキングIOを使用します)。しかし、ライアンの望みはすべてを非ブロッキングにすることです。上のスライド63(内部設計)あなたはライアンが使用する参照libevは、非ブロッキングのための(非同期イベント通知を抽象化ライブラリー)イベントループを。イベントループのため、node.jsはスレッドの数が少なくて済み、コンテキストの切り替え、メモリの消費などを減らします。


11

スレッドは、などの非同期機能を持たない関数を処理するためにのみ使用されstat()ます。

stat()関数はいつもメインスレッド(イベントループ)をブロックせずに実際の呼び出しを実行するためにスレッドを使用する必要性をNode.jsの、ブロッキングされています。これらの種類の関数を呼び出す必要がない場合は、スレッドプールのスレッドが使用されない可能性があります。


7

node.jsの内部動作については何も知りませんが、イベントループを使用すると、スレッドI / O処理よりもパフォーマンスが向上することがわかります。ディスクリクエストを想像して、staticFile.xを与え、そのファイルに対する100リクエストを作成してください。各リクエストは通常​​、そのファイルを取得するスレッドを使用します。つまり、100スレッドです。

パブリッシャーオブジェクトとなる1つのスレッドを作成する最初のリクエストを想像してみてください。他の99個のリクエストはすべて、最初にstaticFile.xのパブリッシャーオブジェクトがあるかどうかを調べます。新しいパブリッシャーオブジェクト。

シングルスレッドが完了すると、100のリスナーすべてにstaticFile.xが渡されてそれ自体が破棄されるため、次のリクエストで新しい新しいスレッドとパブリッシャーオブジェクトが作成されます。

したがって、上記の例では100スレッドと1スレッドですが、100ディスクルックアップではなく1ディスクルックアップでも、ゲインは非常に優れています。ライアンは賢い人です!

別の見方は、映画の冒頭にある彼の例の1つです。の代わりに:

pseudo code:
result = query('select * from ...');

繰り返しますが、データベースに対する100の個別のクエリと...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

クエリが既に実行されている場合、他の同等のクエリは単純にバンドワゴンにジャンプするので、1回のデータベースラウンドトリップで100クエリを実行できます。


3
データベースのことは、他の要求(データベースを使用してもしなくてもかまいません)を保留している間、回答を待たずに、何かを要求し、戻ってきたら電話をかけるという問題です。応答を追跡するのが非常に難しいので、それらを一緒にリンクするとは思わない。また、1つの接続で複数のバッファなし応答を保持できるMySQLインターフェイスはないと思います(??)
Tor Valamo

これは、イベントループがいかに効率を向上できるかを説明するための抽象的な例です。nodejsは、追加のモジュールなしでDBを使用しても何もしません;)
BGerrissen

1
ええ、私のコメントは、1回のデータベースラウンドトリップでの100個のクエリに対するものでした。:p
Tor Valamo

2
こんにちはBGerrissen:いい投稿です。したがって、クエリが実行されているとき、他の同様のクエリは、上記のstaticFile.Xの例のように「リスナー」になりますか?たとえば、100人のユーザーが同じクエリを取得すると、1つのクエリのみが実行され、他の99人は最初のクエリをリッスンしますか?ありがとう!
CHAPa 2011年

1
nodejsが関数呼び出しなどを自動的にメモするように聞こえます。これで、JavaScriptのイベントループモデルで共有メモリの同期について心配する必要がないため、安全にメモリにキャッシュすることが容易になります。しかし、それはnodejsが魔法のようにあなたのためにそれを行うことを意味しているわけではありません。
binki
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.