東陽の研究ノート
記事のディレクトリ
1.シングルスレッドサーバーで一般的に使用されるプログラミングモデル
一般的に使用されるシングルスレッドプログラミングモデルについては、https://blog.csdn.net/qq_22473333/article/details/112910686を参照してください。
高性能ネットワークプログラムの中で、最も広く使用されているモデルは、おそらく「ノンブロッキングIO + IO多重化」モデル、つまりReactorモードです。私は知っています。
l lighttpd、シングルスレッドサーバー。(Nginxは類似していると推定され、チェックされます)
-
libevent / libev
-
ACE、Poco C ++ライブラリ(QT保留中)
-
Java NIO(Selector / SelectableChannel)、Apache Mina、Netty(Java)
-
POE(Perl)
-
ツイスト(Python)
それどころか、boost :: asioとWindowsI / O完了ポートはProactorモードを実装しており、アプリケーション領域は狭いようです。もちろん、ACEはProactorモードも実装していますが、ここには示されていません。
「ノンブロッキングIO + IO多重化」モデルでは、プログラムの基本構造はイベントループです(コードは説明のみを目的としており、さまざまな状況は十分に考慮されていません)。
while (!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = ::poll(fds, nfds, timeout_ms);
if (retval < 0) {
// 处理错误
} else {
// 处理到期的 timers
if (retval > 0) {
// 处理 IO 事件
}
}
}
もちろん、select(2)/ poll(2)には多くの欠点があります。Linuxはepollに置き換えることができ、他のオペレーティングシステムにも対応する高性能の代替手段があります(c10k問題を検索してください)。
Reactor
モデルの利点は明らかであり、プログラミングは単純であり、効率は良好です。ネットワークの読み取りと書き込みを使用できるだけでなく、接続の確立(接続/受け入れ)、さらにはDNS解決も非ブロッキング方式で実行して、同時実行性とスループットを向上させることができます。これは、IOを多用するアプリケーションに適しています。Lighttpd
つまり、内部のfdevent構造は非常にデリケートで、学ぶ価値があります。(ここでは、IOをブロックするという次善の解決策は考慮していません。)
もちろん、高品質のReactorを実装するのはそれほど簡単ではなく、私はオープンソースライブラリを使用したことがないので、ここではお勧めしません。
2.一般的なマルチスレッドサーバーのスレッドモデル
この点に関して私が見つけることができる文書は多くありません、おそらく非常に少ないです:
-
各要求は、ブロッキングIO操作を使用してスレッドを作成します。Java 1.4がNIOを導入する前は、これはJavaネットワークプログラミングの推奨プラクティスでした。残念ながら、スケーラビリティは良くありません。
-
スレッドプールを使用し、ブロッキングIO操作も使用します。1と比較して、これはパフォーマンスを向上させるための手段です。
-
使用し、非ブロッキングIO + IO多重化を。つまり、JavaNIOの方法です。
-
リーダー/フォロワーおよびその他の高度なモード
デフォルトでは、3番目の非ブロッキングIO +スレッドモードごとに1つのループを使用します。
http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#THREADS_AND_COROUTINES
スレッドごとに1つのループ
このモデルでは、プログラム内の各IOスレッドに1つevent loop
(またはReactorと呼ばれる)があり、読み取り、書き込み、およびタイミングイベントの処理に使用されます(定期的または単一の時間に関係なく)。コードフレームワークはセクション2と同じです。
このアプローチの利点は次のとおりです。
-
スレッド数は基本的に固定されており、プログラム起動時に設定でき、頻繁に作成・破棄されることはありません。
-
スレッド間で負荷を割り当てるのは簡単です。
event loop
スレッドのメインループを表します。どのスレッドに動作させる必要がある場合は、タイマーまたはIOチャネル(TCP接続)をそのスレッドのループに登録するだけです。
- リアルタイムのパフォーマンスを必要とする接続では、単一のスレッドを使用できます。
- 大量のデータとの接続はスレッドを独占する可能性があります
- データ処理タスクを他のスレッドに割り当てます。
- 他の二次補助接続はスレッドを共有できます。
重要なサーバープログラムの場合、通常、非ブロッキングIO + IO多重化が使用されます。各接続/アクセプターはReactorに登録されます。プログラムには複数のReactorがあり、各スレッドには最大で1つのReactorがあります。
マルチスレッドプログラムは、Reactorに高い要件を課し线程安全
ます。1つのスレッドが他のスレッドのループに物事を詰め込むことができるようにするには、ループがスレッドセーフである必要があります。
スレッドプール
ただし、IOスレッドには軽いコンピューティングタスクがなく、無駄なイベントループビットを使用blocking queue
します。タスクキューの実装(TaskQueue)を使用する補完的なプログラムがあります。
blocking_queue<boost::function<void()> > taskQueue; // 线程安全的阻塞队列
void worker_thread()
{
while (!quit) {
boost::function<void()> task = taskQueue.take(); // this blocks
task(); // 在产品代码中需要考虑异常处理
}
}
この方法でスレッドプールを実装するのは特に簡単です。
Nの容量でスレッドプールを開始します。
int N = num_of_computing_threads;
for (int i = 0; i < N; ++i) {
create_thread(&worker_thread); // 伪代码:启动线程
}
使い方もとても簡単です。
boost::function<void()> task = boost::bind(&Foo::calc, this);
taskQueue.post(task);
上記の数十行のコードは、単純な固定数のスレッドプールを実装します。これは、Java5のThreadPoolExecutorの特定の「構成」とほぼ同等です。もちろん、実際のプロジェクトでは、これらのコードはグローバルオブジェクトを使用するのではなく、クラスにカプセル化する必要があります。
注意すべきもう1つのこと:Fooオブジェクトの存続期間、他のブログ「デストラクタがマルチスレッドに遭遇したとき-C ++でスレッドセーフなオブジェクトコールバック」では、この問題について詳しく説明しています
http://blog.csdn.net / Solstice / archive / 2010 / 01/22 / 5238671.aspx
タスクキューに加えて、blocking_queue<T>
データを取得するためにコンシューマーを使用することもできます-プロデューサーキュー、つまり、Tは関数ではなくオブジェクトのデータ型であり、データを取得するためのキューコンシューマーが処理されます。これは、タスクキューよりも具体的です。
blocking_queueは、マルチスレッドプログラミングのための強力なツールです。その実装は、Java 5 util.concurrentの(Array | Linked)BlockingQueueを参照できます。通常、C ++は基盤となるコンテナとしてdequeを使用できます。Java 5のコードは非常に読みやすく、コードの基本構造は教科書と同じ(1ミューテックス、2条件変数)であり、堅牢性ははるかに高くなっています。自分で実装したくない場合は、既製のライブラリを使用することをお勧めします。(私は無料のライブラリを使用していません。カオスが推奨されていない場合、興味のある学生Intel Threading Building Blocks
はconcurrent_queueで試すことができます。)
誘導
要約すると、私はお勧めマルチスレッドサーバーのプログラミングモデルは次のとおりです。event loop per thread
+ thread pool
。
- イベントループは、非ブロッキングIOおよびタイマーとして使用されます。
- スレッドプールは計算に使用されます。計算には、タスクキューまたはコンシューマープロデューサーキューを使用できます。
この方法でサーバープログラムを作成するには、Reactorモデルに基づく高品質のネットワークライブラリが必要です。私は社内製品しか使用していません。市場に出回っている一般的なC ++ネットワークライブラリを比較して推奨することはできません。申し訳ありません。
ループやスレッドプールのサイズなど、プログラムで使用する特定のパラメータは、アプリケーションに応じて設定する必要があります。基本原理は、CPUとIOの両方が効率的に動作できるようにするための「インピーダンス整合」です。具体的な話をします。後で検討します。
ここではスレッドの終了についての話はありません。次のブログ「MultithreadedProgrammingAnti-pattern」で説明します。
さらに、logging
プログラム内で特別なタスクを実行する個々のスレッドが存在する場合があります。たとえば、これは基本的にアプリケーションには表示されませんが、システムの容量を過大評価しないように、リソース(CPUとIO)を割り当てるときに含める必要があります。
3.プロセス間通信とスレッド間通信
Linuxでのプロセス間通信(IPC)には、無数の方法があります。UNPv2には、ソケットはもちろんのこと、パイプ、FIFO、POSIXメッセージキュー、共有メモリ、シグナルなどがリストされています。ミューテックス、条件変数、リーダー/ライターロック、レコードロック、セマフォなど、多くの同期プリミティブもあります。
選び方は?私の個人的な経験では、貴重さはそれほど高くはありません。3つまたは4つのものを慎重に選択することで、私の仕事のニーズを十分に満たすことができ、それらを非常にうまく使用でき、間違いを犯しにくいです。
プロセス間通信
私はプロセス間通信にソケットを好みます(主にTCPを参照し、UDPを使用しておらず、Unixドメインプロトコルを考慮していません)。最大の利点は、クロスホスト可能でスケーラビリティがあることです。とにかく、複数のプロセスがあります。1台のマシンの処理能力が不十分な場合は、複数のマシンを使用して処理するのが自然です。プロセスを同じLAN内の複数のマシンに分散し、host:port構成を変更してプログラムを引き続き使用します。それどころか、上記の他のIPCはいずれもマシン間でクロスできず(たとえば、共有メモリが最も効率的ですが、それがどのようであっても、2台のマシンのメモリを効率的に共有することはできません)、スケーラビリティが制限されます。
プログラミングでは、TCPソケットとパイプの両方がバイトストリームの送受信に使用されるファイル記述子であり、どちらも読み取り/書き込み/ fcntl / select / pollなどを実行できます。違いは、TCPが双方向、パイプが一方向(Linux)、プロセス間の双方向通信のために2つのファイル記述子を開く必要があることです。これは不便です。また、パイプを使用するには、プロセスに親子関係が必要です。 、パイプの使用を制限します。バイトストリームを送受信する通信モデルでは、ソケット/ TCPほど自然なIPCはありません。もちろん、パイプには古典的なアプリケーションシナリオもあります。これは、Reactor / Selectorを作成するときにselect(または同等のpoll / epoll)呼び出しを非同期的にウェイクアップするために使用されます(Sun JVMはLinuxでこれを行います)。
TCPポートはプロセスによって排他的に所有され、オペレーティングシステムは自動的にそれを再利用します(確立された接続のリスニングポートとTCPソケットは両方ともファイル記述子であり、オペレーティングシステムはプロセスが終了するとすべてのファイル記述子を閉じます)。これは、プログラムが予期せず終了した場合でも、システムにゴミが残らないことを示しています。プログラムを再起動した後は、オペレーティングシステムを再起動しなくても、比較的簡単に回復できます(クロスプロセスミューテックスを使用するリスクがあります)。別の利点があります。ポートは排他的であるため、プログラムの再起動を防ぐことができ(後者のプロセスはポートを取得できず、当然機能しません)、予期しない結果が発生します。
2つのプロセスはTCPを介して通信します。一方がクラッシュすると、オペレーティングシステムが接続を閉じるため、もう一方のプロセスはほとんどすぐに接続を認識し、すぐにフェイルオーバーできます。もちろん、アプリケーション層のハートビートも欠かせません。今後、サーバーの日時処理については、ハートビートプロトコルの設計についてお話します。
他のIPCと比較すると、TCPプロトコルの自然な利点は可记录可重现
、tcpdump / Wiresharkが2つのプロセス間のプロトコル/状態の競合を解決するための優れたヘルパーであるということです。
さらに、ネットワークライブラリに「接続の再試行」機能がある場合、システム内のプロセスを特定の順序で開始する必要はなく、プロセスを個別に再起動することもできます。これは、信頼性の高い分散システム。
TCPバイトストリーム通信を使用すると、マーシャル/アンマーシャルのオーバーヘッドが発生します。これには、適切なメッセージ形式、正確にはワイヤ形式を選択する必要があります。これは私の次のブログの主題になるでしょう、そして私は今のところそれをお勧めしますGoogle Protocol Buffers
。
2つのプロセスが同じマシン上にある場合、共有メモリを使用するか、TCPを使用すると言う人がいるかもしれません。たとえば、MS SQLServerは両方の通信方法を同時にサポートします。私は尋ねました、そのような少しのパフォーマンス改善のためにコードの複雑さを大幅に増やすことは価値がありますか?TCPはバイトストリームプロトコルであり、順次読み取りのみが可能で、書き込みバッファがあります。共有メモリはメッセージプロトコルです。プロセスaは、プロセスbが読み取るためにメモリのブロックをいっぱいにします。これは、基本的に「待機停止」方式です。これら2つの方法を1つのプログラムに組み合わせるには、2つのIPCをカプセル化する抽象化レイヤーを構築する必要があります。これにより不透明性がもたらされ、テストの複雑さが増します。通信の一方の当事者がクラッシュした場合、状態の調整はソケットよりも厄介になります。私には取られません。さらに、数万ドルで購入したSQL Serverのプログラムとマシンリソースを共有してもよろしいですか?製品のデータベースサーバーは、多くの場合、独立した高構成サーバーであり、通常、他のリソースを大量に消費するプログラムを同時に実行することはありません。
TCP自体はデータストリームプロトコルです。TCPを直接使用して通信するだけでなく、その上にRPC / REST / SOAPなどの上位層の通信プロトコルを構築することもできますが、これはこの記事の範囲を超えています。さらに、ポイントツーポイント通信に加えて、アプリケーションレベルのブロードキャストプロトコルも非常に便利であり、かなりの制御可能な分散システムを簡単に構築できます。