非同期ソリューションの原則について

プログラマーのための素晴らしい新年の読書にあなたの注意を喚起します:)彼が書いたAlexander Chistyakovalexclear )による記事は、HighLoad ++ 2017のレポートMons Andersoncodesign )の論文に触発されました。



アレクサンダー・チスチャコフ



非同期ソリューションがどのように機能するかについて話し Mons Anderson によって提案された分類を見てみましょう。 独自の分類を提案できる場合があります。



既存のソリューションを分類するために、最初に座標軸を考えます。 開発エンジニアの観点から見ると、「 同期 」と「 非同期 」のパラダイムは、アプリケーションの複雑さと「効率」(まだ決定されていない「効率」)の両方が異なる抽象化に基づいています。



筋金入りのハードコアに注意してください!



アプリケーションの複雑さに関して、同期プログラミングは非同期よりも簡単であると考えられています。 まず、同期パラダイムを使用する場合、アルゴリズム内の命令のシーケンスに違反しません。 第二に、変数のスコープはより簡単に制御されます。



非同期パラダイム



非同期パラダイムが解決するタスクを理解するために、低レベルでのプログラムの動作を考えてみましょう。



OSスケジューラー



オペレーティングシステムの観点から、プログラムアルゴリズムはコンピューターリソースを利用します。コンピューターリソースの主なものは、プロセッサー(CPU)とメモリです。 オペレーティングシステムスケジューラは、プロセスをプロセッサコアの1つに割り当てる役割を果たします。



プロセッサコアのプロセスを変更する場合、コンテキストを切り替え、現在のすべてのプロセッサレジスタをメモリに保存し、実行対象のプロセスまたはスレッドに対応するメモリからプロセッサレジスタを復元する必要があります。 この操作には、最新のプロセッサ(非常に高価です)で約200プロセッササイクルかかります。



I / O操作はDMAメカニズムを使用して実行されますが、CPUはプロセスに関与しません。 Linuxでは、I / Oを実行するプロセスがCPUに割り当てられていると正式に説明されているという事実にもかかわらず、実際、そのようなプロセスはプロセッサを占有せず、無停電スリープ(D状態)状態になります。 1つのプロセスによるI / Oの時点で、スケジューラは別のプロセスをプロセッサに割り当てます。



OSスケジューラの観点から見ると、OSスレッド(少なくともLinuxの場合)は単なるプロセスです-それぞれが独自の独立したコンテキストを持ち、スレッドの1つの入力/出力操作をブロックしても、他のスレッドの作業には影響しません。



したがって、プロセッサでの実行からOSスレッドが削除されるのは、次の2つの場合です。

  1. 実行に割り当てられたタイムスロットが終了しました。
  2. スレッドは同期I / O操作(その完了を待機する必要がある操作)を実行しました。




Java



古典的な(現代のJavaのような)同期並列処理モデルは、オペレーティングシステムのスレッドを言語のランタイム(仮想マシン)スレッドとして使用することに焦点を当てています。 厳密に言えば、ランタイムと仮想マシンは異なる概念ですが、私たちにとっては重要ではありません。



プログラムがクライアント接続を提供する場合、 各クライアントは接続を維持するために個別のオペレーティングシステムスレッドを必要とします 。 この場合、オペレーティングシステムのスレッド間のコンテキストスイッチの数は、クライアント接続の数に比例します(上記の「効率」を参照)。

10,000個のクライアント接続を提供する必要があるとします- 技術的には10,000個のオペレーティングシステムのスレッドを生成する必要があります


Perl



Javaのようなこのマルチスレッドモデルは、GILにより、現代言語のほとんどのインタープリター(Python、Ruby(Rubyのスレッドはオペレーティングシステムのスレッドではありません))では機能しません。 Perlの標準マルチスレッドはJavaと同じ原則に従って実装されます-PerlスレッドはOSスレッドですが、各スレッドは独自のインタープリターによって実行されます(「効率」を参照-これはJavaのスレッドよりもはるかに効率が悪い)。

同じ10,000個の接続を提供する必要があると仮定します。今では、Perlインタープリターの10,000個の個別のコピーを生成する必要があります。


N:1



OSストリームの代わりに、接続された多数のクライアントの大量入出力中のコンテキストスイッチングを最適化するために、協調マルチタスクメカニズムのエミュレーションがランタイムまたはプログラミング言語インタープリターで直接使用されます。



この場合、「スレッド」はインタープリタープロセス内にのみ存在し、OSスケジューラーはそれらについて何も知りません。 したがって、インタープリターはそのようなフロー間の切り替えを行う必要があり、2つのケースでこれを行います:ストリームが明示的にyieldコマンドまたは別のストリームに制御を転送する同様のコマンドを実行するか、ストリームがI / O操作を実行します。 このようなマルチスレッドモデルは「N:1」と呼ばれますインタープリターレベルのいくつかのスレッドは、OSカーネルレベルの1つのスレッドに対応します



ただし、I / O操作が同期の場合、OSレベルのスレッドはD状態になり、I / O操作が終了するまでプロセッサで中断されます。 これにより、このOSスレッドで実行されているすべてのNスレッドは、そのうちの1つのI / O操作が終了するまでブロックされます(「効率」を参照)。



コールバック



幸いなことに、カーネルは非同期(一部の予約を含む)I / Oメカニズムを提供します。これを使用すると、OSレベルの呼び出しスレッドはI / O操作の終了を待たずに実行を継続します。 この場合、I / O操作の最後に、ユーザーが登録したコールバックが呼び出されます。



非同期I / Oを使用するには、 fcntl(2)システムコールを使用してソケットを非同期モードにするだけで十分です。

10,000個の接続を提供する必要があると想像してください。これを行うには、10,000個の開いているソケットで読み取りコマンドを実行する必要があります。


効率を上げるために、オープンファイル記述子をカーネル内の共通データ構造に結合し、ソケットのグループで非同期I / Oを実行できるメカニズムが発明されました。 当初、このようなメカニズムはselect(2)システムコールでした。 selectを呼び出す際の問題は、イベントの発生時に、登録されたすべてのファイル記述子を反復処理して、イベントの発生を判別する必要があることです。そのような列挙のアルゴリズムの複雑さは、開いているファイル記述子の数に比例します。



必要なソケットを見つけるための一定のアルゴリズムの複雑さを保証するために、 kqueue (FreeBSD)およびepoll(7) (Linux)メカニズムが実装されました。



epollを使用する場合、OSレベルの実行スレッドは、開いているファイル記述子の登録/削除、非同期I / O呼び出しの準備、トリガーされたコールバックの処理でビジーです。 プログラムがyieldを使用しない場合、インタープリター(またはランタイム)レベルのスレッド間のプロセッサーリソースの公平な分配に違反するため、入出力操作間でCPUを集中的に使用するコンピューティングを防ぐことが重要になります。



GolangおよびNode.JS



Golangランタイムメカニズムについて説明しました 。 唯一の違いは、GolangのマルチスレッドメカニズムがN:1ではなく、N:Mであるということです。Mはプロセッサコアの数です。 Rantime Golangは、入出力時だけでなく、他の関数の呼び出し時にもゴルーチンを自動的に切り替えることができます(この場合、無限ループは対応するOSストリームのプロセッサー時間の100%を使用し、ランタイムによって終了することはありません)。



Node.JSインタープリターもepollを中心に(より正確にはnginxのコードを中心に)構築されますが、N:1モデルを使用するだけで 1つのコアをスケーリングしません。



場合によっては、Golangランタイムスケジューラに類似したスケジューラがライブラリまたはトランスパイラー(PerlのCoroやBabelを使用したJSのasync / awaitなど)として実装されているため 、コルーチンをインタープリターレベルでサポートしない言語で使用できます。



分類の試み



上記に基づいて、マルチスレッド回路の次の分類を提案します。

  1. OSストリームを介したランタイムストリームの古典的な実装。
  2. CorutinタイプN:1またはN:Mの実装。
  3. 手動でコールバックを登録し、適切なヌードルを書き込むことにより、非同期の入力/出力を使用した低レベルの作業(どこかにコンテキストのハッシュマップがあることを忘れないでください)。


HTTP要求の処理に関するMonsの分類
さて、モンス分類へ。 私が理解しているように、これはHTTPリクエストを処理するタスクを中心に構築されており、Apache Webサーバーの古典的な用語を使用しています。



どうやら、 単一のプロセスサーバーは、一度に1つの要求のみを処理できる同期実行サーバーにすぎません。



Forkingサーバーは、処理された要求ごとに個別のプロセスを生成するサーバーです(Linux fork(2)では「効率」を参照してください。そうでない場合は悪化します)。



PreforkingサーバーはApacheの世界の古典であり、一定量のワークフローを事前に作成しますが、処理は依然として同期的です。



コールバックはコルーチンよりも悪いという事実について、自分自身に違いを感じ、最初にコールバックでコードを書き、次にコルーチンでコードを書くか、単にnginxのソースコードを勉強するために言った。 人々がコールバックコードを自分の人生で複数回作成したいのですが、登山またはパラシュートセクションにサインアップする方が良いかどうかはわかりません。



非同期プリフォークとは-明らかに、これはMワークプロセスが実行されているときのN:Mメカニズムの実装です。



私が知らない非同期+ワーカーとは何ですか?ワーカーはApacheの世界ではプリフォークとは異なるため、覚えている限りでは、ワーカーフローはワーカースキームの代わりにワークフローを生成します(OSの観点からは違いはありませんが、共有状態の観点からは違いがあります) 、そして、可変共有状態は、最初に奪われてから解雇される理由です)。



マルチスレッド非同期とは何ですか? 私の意見では(私自身ではなく、怠myselfで罪深い私自身は何も発明しなかった)分類は再びN:Mであり 、なぜ同じものに3つの名前があるのか​​わかりません。



「効率」が何であるかはまだ決定していません。 必要ありません。



PS:ちなみに、当時は報告書は鳴りませんでした(報告書の準備はできていませんでしたが、本当に望んでいましたが)。 どこで議論を続けましょう:)



All Articles