高同時実行におけるスレッドとスレッド プールの詳細な説明

すべてはCPUから始まります

マルチスレッドについて話すときに、なぜ CPU から始めるのかと疑問に思われるかもしれません。理由は簡単で、流行の概念が存在せず、問題の本質がより明確に見えるからですCPU はスレッドやプロセスなどの概念を知りません。CPU は次の 2 つのことだけを知っています: 1. メモリから命令をフェッチする 2. 命令を実行して 1 に戻る

ご存知のとおり、ここでは CPU はプロセスとスレッドの概念を実際には認識していません。次の疑問は、CPU がどこから命令をフェッチするのかということです。答えは、よく知られているプログラムカウンタであるプログラムカウンタ(以下PC)というレジスタです。ここではレジスタをあまり難しく考えず、単純にメモリとして理解していただければ良いのですが、アクセス速度はただ速くなります。PCレジスタには何が保存されますか? ここに保存されているのはメモリ内の命令のアドレスですが、どの命令ですか? CPU が次に実行する命令です。

では、誰が PC レジスタに命令アドレスを設定するのでしょうか? PC レジスタのアドレスはデフォルトで自動的に 1 ずつインクリメントされることがわかりましたが、これは当然のことです。ほとんどの場合、CPU は 1 つずつ順番に実行するためです。if と else が発生すると、この順番の実行は中断されます。 CPU がこの種の命令を実行すると、計算結果に応じて PC レジスタの値が動的に変更されるため、CPU は実行する必要のある命令に正しくジャンプできます。PC の初期値はどのように設定されているのですか? この質問に答える前に、CPU によって実行される命令がどこから来たのかを知る必要があります。それはメモリから来ます、ナンセンスです、メモリ内の命令はディスクに保存された実行可能プログラムからロードされます、ディスク内の実行可能プログラムはコンパイラによって生成されます、そしてコンパイラはどこで機械語命令を生成しますか?答えは私たちが定義した関数です。

これは関数であることに注意してください。関数がコンパイルされると、CPU によって実行される命令が形成されます。では当然、CPU に関数を実行させるにはどうすればよいでしょうか? 明らかに、関数のコンパイル後に形成された最初の命令を見つける必要があるだけであり、最初の命令は関数のエントリです。ここで、CPU に関数を実行させたいことがわかりました。その関数に対応する最初の機械語命令のアドレスを PC レジスタに書き込むだけで、記述した関数が CPU によって実行され始めます。 。これはスレッドとどのような関係があるのでしょうか?という疑問があるかもしれません。

CPUからOSまで

前のセクションでは、CPU の動作原理を理解しました。CPU に特定の機能を実行させたい場合、その機能に対応する最初のマシン実行を PC レジスタにロードするだけで済みます。オペレーティング システムがなくても関数を実行する CPU 実行プログラムは実行可能ですが、これは非常に面倒なプロセスです。

  • メモリ内で適切なサイズの領域ローダーを見つける
  • 関数エントリを見つけて、PC レジスタを設定し、CPU にプログラムの実行を開始させます。

この 2 つのステップは決して簡単ではありませんが、プログラムを実行するたびに上記 2 つの処理を手動で実装すると頭がおかしくなってしまいますので、賢いプログラマであれば、上記 2 つのステップを自動的に完了するプログラムを簡単に作成したいと考えます。ステップ。

機械語命令は実行のためにメモリにロードする必要があるため、メモリの開始アドレスと長さを記録する必要があり、同時に関数のエントリ アドレスを見つけて PC レジスタに書き込む必要があります。この情報を記録するにはデータ構造が必要です。

struct *** {
   void* start_addr;
   int len;
   
   void* start_point;
   ...
};

続いては名付けの時間です。このデータ構造には常に名前が必要です。この構造はどのような情報を記録するために使用されますか? 記録されるのは、プログラムがメモリにロードされたときの実行状態です。ディスクからメモリにロードされたときのプログラムの名前は何ですか? それをプロセスと呼んでください。私たちの基本原則は、神秘的に聞こえなければならないということです。つまり、誰にとっても理解するのは簡単ではありません。私はそれを「理解できない原則」と呼んでいます。そしてそのプロセスが生まれました。CPU によって実行される最初の関数にも名前があり、最初に実行される関数の方が重要に思えるので、単にmain 関数と呼ぶことにします。上記の 2 つのステップを完了するプログラムにも名前を付ける必要がありますが、「原理が理解されていない」によれば、この「単純な」プログラムはオペレーティング システム(Operating System) と呼ばれます。このようにしてオペレーティング システムが誕生し、プログラマーはプログラムを実行する場合に手動でオペレーティング システムを再度ロードする必要がなくなりました。プロセスと OS が配置されたので、すべてが完璧に見えます。

シングルコアからマルチコアへ、マルチコアを使いこなすには

人間の特徴の一つは、人生がシングルコアからマルチコアへと常に揺れ動いていることです。

このとき、プログラムを書いてマルチコアを使いたい場合はどうすればよいでしょうか?学生の中には、プロセスが存在しないので、さらにいくつかのプロセスを開くだけで十分だと言う人もいるかもしれません。それは合理的に思えますが、いくつかの問題があります。

  • プロセスはメモリ空間を占有する必要があります (前の省電力の説明を参照)。複数のプロセスが同じ実行可能プログラムに基づいている場合、これらのプロセスのメモリ領域の内容はほぼ同一となり、明らかにメモリの無駄が発生します。
  • コンピュータによって処理されるタスクは、プロセス間通信を伴うため、より複雑になる場合があります。各プロセスは異なるメモリ アドレス空間にあるため、プロセス間通信には当然オペレーティング システムの助けが必要となり、プログラミングやプログラミングの難易度が高まります。システムの複雑さが増大します。

それについて私たちは何ができるでしょうか?

プロセスからスレッドへ

この問題をもう一度よく考えてみましょう. いわゆるプロセスは、CPU によって実行される機械語命令と関数の実行時のスタック情報を格納するメモリの一部にすぎません. プロセスを実行するには、 main 関数 最初の機械命令のアドレスが PC レジスタに書き込まれ、プロセスが実行を開始します。

プロセスの欠点は、エントリ関数、つまりメイン関数が 1 つだけであるため、プロセス内の機械語命令は1 つの CPU でしか実行できないことです。したがって、複数の CPU で機械語命令を実行する方法はありますか?同じプロセスですか?main 関数の最初の命令のアドレスを PC レジスタに書き込むことができるので、他の関数と main 関数の違いは何なのかを考えることができるはずです。答えは、違いはありません。 main 関数の特別な特徴は、それが CPU によって実行される最初の関数であるということです。 それについて特別なことは何もありません。PC レジスタを main 関数にポイントすることができ、そして、次のことを行うことができますPC を任意の機能に登録しますPC レジスタがメイン関数以外を指すようにすると、スレッドが生成されます

プロセスには複数のエントリ関数が存在する可能性があり、これは、同じプロセスに属する機械語命令が複数の CPU によって同時に実行される可能性があることを意味しますこれはプロセスとは異なる概念であることに注意してください。プロセスを作成するときは、プロセスをロードするためにメモリ内の適切な領域を見つけてから、CPU の PC レジスタを main 関数にポイントする必要があります。つまり、実行は 1 回だけです。プロセスの流れを説明します

しかし、現在は異なります。複数の CPU が、同じ屋根 (プロセスが占有するメモリ領域) の下で、プロセスに属する複数のエントリ関数を同時に実行できます。つまり、1 つのプロセス内に複数の実行フローが存在できるようになりました

いつも実行フローと呼ぶのはちょっと分かりやすすぎるので、またしても「原理が分からない」を犠牲にして、分かりにくい名前を付けてスレッドと呼びましょう。ここでスレッドの出番です。オペレーティング システムはプロセスごとに、プロセスのメモリ空間などを記録するために使用される一連の情報を保持しており、この一連の情報はデータ セット A として記録されます。同様に、オペレーティング システムも、スレッドのエントリ関数やスタック情報を記録するために使用されるスレッドの大量の情報を維持する必要があり、このデータの山はデータ セット B として記録されます。明らかに、データセット B の量はデータ A よりも少ないです。同時に、スレッドはアドレス空間で実行されるため、プロセスと異なり、スレッドを作成するときにメモリ内にメモリ空間を見つける必要はありません。このアドレス空間はプログラム内にあり、起動時に作成され、スレッドはプログラムの実行中 (プロセスの開始後) に作成されるため、スレッドが実行を開始すると、このアドレスがスペースはすでに存在しており、スレッドを直接使用できます。さまざまな教科書で取り上げられているスレッドの作成がプロセスの作成よりも速いのはこのためです (もちろん他の理由もあります)。スレッドの概念では、プロセスの開始後に複数のスレッドを作成するだけですべての CPU をビジー状態に保つことができ、これがいわゆる高パフォーマンスと高同時実行性の根源であることに注意してください

非常に簡単で、適切な数のスレッドを作成するだけですもう 1 つの注目すべき点は、各スレッドがプロセスのメモリ アドレス空間を共有するため、スレッド間の通信がオペレーティング システムに依存する必要がなく、プログラマにとっては非常に便利であり、トラブルが絶えないということです。スレッド間通信は便利すぎるため、エラーが非常に発生しやすいという事実があります。エラーの根本は、CPU が命令を実行するときにスレッドの概念を持たないことです。マルチスレッド プログラミングが直面する相互排除同期の問題は、プログラマー自身が解決する必要があります。詳細な説明があります。最後に思い出していただきたいのは、スレッドの使用法を説明する前の図では複数の CPU が使用されていますが、複数のスレッドを使用するために複数のコアを使用する必要があるという意味ではありません。単一コアの場合は、複数のスレッドを使用することもできます。その理由は、スレッドはオペレーティング システム レベルで実装されており、コアの数とは関係がありません。CPU が機械語命令を実行するとき、実行された機械語命令がどのスレッドに属しているかを認識しません。 。CPU が 1 つしかない場合でも、オペレーティング システムは、スレッド スケジューリングを通じて各スレッドを「同時に」進めることができます。「実行」しますが、実際には、常に 1 つのスレッドだけが実行されます。

 電車による情報: Linux カーネル ソース コード技術学習ルート + ビデオ チュートリアル カーネル ソース コード

電車で学ぶ: Linux カーネル ソース コード メモリ チューニング ファイル システム プロセス管理 デバイス ドライバー/ネットワーク プロトコル スタック

スレッドとメモリ

前の説明で、スレッドと CPU の関係はわかりました。つまり、スレッドが実行できるように、CPU の PC レジスタがスレッドのエントリ関数を指すようにする必要があります。そのため、作成時にエントリ関数を指定する必要があります。スレッド。使用するプログラミング言語に関係なく、スレッドの作成方法はほぼ同じです。

// 设置线程入口函数DoSomething
thread = CreateThread(DoSomething);

// 让线程运行起来
thread.Run();

では、スレッドとメモリにはどのような関係があるのでしょうか? 関数が実行されたときに生成されるデータには、関数のパラメータローカル変数戻りアドレスなどの情報が含まれていることがわかっており、これらの情報はスタックに格納されます。スレッドという概念がまだ登場していない時点では、実行フローは 1 つだけです。プロセスなのでスタックは 1 つだけあり、スタックの一番下はプロセスのエントリ関数、つまり main 関数です。図に示すように、main 関数が funA を呼び出し、funcA が funcB を呼び出すとします。

では、スレッドを立てた後はどうでしょうか?スレッドを作成した後、プロセス内に複数の実行エントリが存在します。つまり、同時に複数の実行フローが存在します。その場合、実行フローが 1 つだけのプロセスには、ランタイム情報を保存するためにスタックが必要です。各実行フローの情報を保存するために複数のスタックが使用されます。つまり、オペレーティング システムは、プロセスのアドレス空間内の各スレッドにスタックを割り当てる必要があります。つまり、各スレッドは独自のスタックを持ちます。これを認識することは非常に重要です。

同時に、スレッドの作成によりプロセス メモリ領域が消費されることもわかりますが、これも注目に値します。

糸の使用

スレッドの概念がわかったところで、次はプログラマとしてスレッドをどのように使用すればよいでしょうか? ライフサイクルの観点から見ると、スレッドで処理されるタスクには、長いタスクと短いタスクの 2 種類があります。1. 長いタスク、長生きするタスクは、名前が示すように、タスクが長期間存続することを意味します。たとえば、私たちがよく使う単語を例に挙げると、単語で編集したテキストはディスクに保存する必要があります。 , そして、データをディスクに書き込むのはタスクです。この時点でより良い方法は、ディスクに書き込むためのスレッドを作成することです。書き込みスレッドのライフ サイクルはワード プロセスのライフ サイクルと同じです。 Word が開かれると書き込みスレッドが作成され、ユーザーが Word を閉じるとスレッドが破棄されます。これは長いタスクです。

このシナリオは、比較的単純な特定のタスクを処理する専用スレッドを作成する場合に非常に適しています。長いタスクと、それに応じて短いタスクがあります。

2. 短いタスク。存続期間の短いタスクの概念も非常に単純です。つまり、ネットワーク リクエスト、データベース クエリなど、タスクの処理時間が非常に短く、そのようなタスクはすぐに処理して完了できます。短い時間で。したがって、Web サーバー、データベース サーバー、ファイル サーバー、メール サーバーなどのさまざまなサーバーで短いタスクがより一般的であり、これはインターネット業界の学生にとって最も一般的なシナリオでもあり、私たちが注目したいのはこのシナリオです。 。このシナリオには、タスクの処理時間が短いことと、タスクの数が膨大であることの 2 つの特徴がありますもしあなたがこの種の仕事を任されたらどうしますか?これは非常に単純で、サーバーがリクエストを受信すると、タスクを処理するスレッドを作成し、処理が完了した後にスレッドを破棄するだけなので、とても簡単だと思うかもしれません。このメソッドは通常、スレッドごとのリクエストと呼ばれます。これは、リクエストに対してスレッドが作成されることを意味します。

長いタスクの場合、この方法は非常にうまく機能しますが、多数の短いタスクの場合、この方法は実装が簡単ですが、いくつかの欠点があります。 1. 前のセクションからわかるように、スレッドはオペレーティング システムの一部という概念 (ユーザー モード スレッド、コルーチンなどの実装についてはここでは説明しません) であるため、スレッドの作成は当然オペレーティング システムの助けを借りて完了する必要があり、 2. 各スレッドは独自の独立したスタックを持つ必要があるため、多数のスレッドが作成されると、メモリやその他のシステム リソースが過剰に消費されます。これは工場のオーナーのようなものです。 (考えてみてください、あなたは幸せですか?) そして、あなたはたくさんの注文を持っています。注文のバッチが届くたびに、労働者のバッチを雇う必要があります。生産される製品は非常にシンプルで、労働者は迅速に処理してください。この一連の注文を処理した後、採用に熱心に取り組んでいた労働者は解雇されます。新しい注文が入ったら、一生懸命採用します。労働者は 1 回、5 分間働き、10 分間人材を採用します。会社を倒産させたくなければ、おそらく倒産させないでしょう。したがって、より良い戦略は、グループで人材を採用し、その場で人材を育てることです。命令はなく、命令がないときは誰もが何もせずに過ごすことができます。

これがスレッド プールの起源です。

マルチスレッドからスレッドプールへ

スレッド プールの概念は非常に単純です。スレッドのバッチを作成し、その後それらを解放しないだけです。タスクは処理のためにこれらのスレッドに送信されるため、スレッドを頻繁に作成および破棄する必要はありません。同時に、スレッド プール内のスレッドの数により、通常は固定されており、あまり多くのメモリを消費しないため、ここでのアイデアは、 を再利用して制御可能にすることです。

スレッドプールの仕組み

学生の中には、タスクをスレッド プールに送信するにはどうすればよいのかと疑問に思う人もいるかもしれません。これらのタスクはスレッド プール内のスレッドにどのように与えられるのでしょうか? 明らかに、データ構造内のキューはこのシナリオに自然に適しています。タスクを送信するプロデューサーはプロデューサーであり、タスクを消費するスレッドはコンシューマーです。実際、これは古典的なプロデューサーとコンシューマーの問題です

これで、オペレーティング システムのコースが教えられ、面接でこの質問が行われる理由がわかるはずです。プロデューサーとコンシューマーの問題を理解していなければ、基本的にスレッド プールを正しく書くことができないからです。スペースの制限のため、ブロガーはここで生産者と消費者の問題について詳しく説明するつもりはありません。答えはオペレーティング システムの関連情報を参照することで得られます。ここでブロガーは、スレッド プールに送信されるタスクが一般的にどのようなものであるかについて話すつもりです。一般に、スレッド プールに送信されるタスクは、1) 処理されるデータ、2) データを処理する関数の2 つの部分で構成されます。

struct task {
void* data;     // 任务所携带的数据
    handler handle; // 处理数据的方法
}

(コード内の構造体をクラス、つまりオブジェクトとして理解することもできます。) スレッド プール内のスレッドはキュー上でブロックされます。プロデューサーがデータをキューに書き込むと、スレッド プール内のスレッドがキューにブロックされます。目覚めた後、スレッドはキューから上記の構造体 (またはオブジェクト) を取り出し、構造体 (またはオブジェクト) 内のデータをパラメーターとして受け取り、処理関数を呼び出します。

while(true) {  struct task = GetFromQueue(); // 从队列中取出数据  task->handle(task->data);     // 处理数据}

上記はスレッド プールのコア部分です。これらを理解すると、スレッド プールがどのように機能するかを理解できます。

スレッド プール内のスレッドの数

スレッド プールがあるので、スレッド プール内のスレッドの数はいくつでしょうか? 次に進む前に、これについて自分で考えてください。これが見えたらまだ寝ていません。スレッド プール内のスレッドが少なすぎると CPU を最大限に活用できず、作成されるスレッドが多すぎると、システム パフォーマンスの低下、過剰なメモリ使用、スレッドの切り替えによる消費などが発生することを知っておく必要があります。したがって、スレッド数は多すぎても少なすぎてもいけませんが、どのくらいにすればよいのでしょうか。この質問に答えるには、スレッド プールが処理するタスクの種類を知る必要がありますが、学生の中には 2 種類あると答えた人もいるかもしれません。長いタスクと短いタスク、これはライフサイクルの観点からの話なので、タスクを処理するために必要なリソースの観点からも 2 つのタイプがあり、何も探す必要がなく抽象的です。ああ、いいえ、CPU と I/O を集中的に使用します。1. CPU 集中型いわゆる CPU 集中型とは、処理タスクが科学計算や行列演算などの外部 I/O に依存する必要がないことを意味します。この場合、スレッド数とコア数が基本的に同じであれば、CPU リソースを最大限に活用できます。

2. I/O 集中型のタスクでは、計算部分にはそれほど時間がかからず、ほとんどの時間がディスク I/O やネットワーク I/O などに費やされます。

この場合、少し複雑になります。パフォーマンス テスト ツールを使用して、I/O 待機に費やされた時間を評価する必要があります。ここでは WT (待機時間) として記録され、CPU の計算に必要な時間はここに記録されます。 CT (計算時間) とすると、N コア システムの場合、I/O 待機時間が計算時間と同じであると仮定すると、適切なスレッド数はおそらく N * (1 + WT/CT) になります。 CPU リソースを完全に活用するには約 2N スレッドが必要ですが、これは理論上の値にすぎず、実際のビジネス シナリオに従って特定の設定をテストする必要があることに注意してください。もちろん、考慮する必要があるのは CPU を最大限に活用することだけではなく、スレッド数が増加するにつれて、メモリ使用量、システム スケジューリング、開いているファイルの数、開いているソケットの数、開いているデータベース リンクのすべてが考慮されるようになります。考慮する必要があります。したがって、ここに普遍的な公式はなく、ケースバイケースで分析する必要があります。

スレッド プールは万能薬ではありません

スレッド プールはマルチスレッドの一種にすぎないため、デッドロックの問題や競合状態の問題など、マルチスレッドが直面する問題も避けられません。この部分については、オペレーティング システムの関連情報も参照できます。答えを導き出すシステムですから、基礎がとても重要なのです、オールドアイアン。

スレッド プールの使用に関するベスト プラクティス

スレッド プールはプログラマにとって強力な武器です。スレッド プールはインターネット企業のほぼすべてのサーバーで見られます。スレッド プールを使用する前に、次のことを考慮する必要があります。

  • タスクが長いタスクか短いタスクか、CPU 集中型か I/O 集中型かにかかわらず、タスクを完全に理解します。両方がある場合は、これら 2 種類のタスクを別のスレッドに配置することをお勧めします。プール、これはスレッド数を決定するより良い方法かもしれません
  • スレッド プール内のタスクに I/O 操作がある場合は、必ずこのタスクにタイムアウトを設定してください。そうしないと、タスクを処理するスレッドが永久にブロックされる可能性があります。
  • スレッド プール内の他のタスクの結果を同期的に待たないことをお勧めします。

要約する

このセクションでは、CPU から始まり、一般的に使用されるスレッド プールに至るまで、下位層から上位層まで、ハードウェアからソフトウェアまで説明します。この記事には特定のプログラミング言語がないことに注意してください。スレッドは言語レベルの概念ではありません (ユーザー モード スレッドはまだ考慮されていません)。しかし、スレッドを本当に理解すれば、どの言語でも多くのスレッドを使用できると思います。理解する必要があるのはタオであり、その先に芸術があるのです。この記事がスレッドとスレッド プールの理解に役立つことを願っています。

原作者:コードファーマーの無人島サバイバル

 

おすすめ

転載: blog.csdn.net/youzhangjing_/article/details/132190325