学習システムプログラミング 第31回 【マルチスレッドの排他と同期】

序章:

北京時間: 2023/7/16/14:32、今のところ、この面で私以上にできる人は誰もいません、ははは、楽観的です!補わなければならないことがたくさんあります。スレッド関連の知識を学び終えると、システムプログラミングはほぼ完了します。以前に学んだシステム知識で十分です。より広範な拡張は自分で検討する必要があります。もちろん、これまでの関連知識がある程度復習されていることが前提ですが、いつ行われるかは分かりませんが、少しずつ進めていきましょう!もちろん、長い間勉強したからには無駄ではなく、アーキテクチャやその基礎となる原理などの知識をしっかりと理解しておく必要がありますが、復習は理解の強化と確認の定着、そして詳細な知識の定着を目的としています。 , A モジュールについては、大まかな方向性は存在しますが、具体的な内容は曖昧で、より重要なのはコードの操作です。もちろん、この具体的な部分はレビューの時間を待たなければなりません。私にとって、より重要なことは、もちろん、問題は、コーディング能力です。家に帰ってから、あまりスケジュールを調整できていないのが残念です。ドラマを見るか、小説を読むか、チェスをするか、画面をスワイプするのが得意です。勉強する時間はほとんどありません。コントロールできません。それは。プレイしているときはどんなに幸せだろう。それは同じくらい苦痛だ。一週間以上ひどい状態が続いた後、私たちは見られるドラマ、読める小説、そしてチェスの昇級すべきランクをすべて修正したで、それらをすべてアンインストールしました。今、私たちを悪くする唯一のことは、ビデオ画面のスワイプです。WeChat がアンインストールできないことがなければ、そうでなければ、私たちはほとんど無敵でしょう。今しなければならないのはスケジュールの調整です、1日でも2日でも早く寝ていれば、予定通り前に進めると思います、ナンセンスです もっと言うと、今日が最も重要なターニングポイントです夏休み、収録語数が増えました、次のブログ紹介、達成できるか見てみましょう!今回は前回のブログからプロセス制御に関する知識を引き継ぎ、プロセスの同時実行性やその他の知識について見ていきましょう!

ここに画像の説明を挿入

徹底したスレッド制御

前回のブログでは、スレッド制御の学習はもちろん、スレッド作成、スレッド終了、スレッド待機、そしてスレッド終了の4つの方法について詳しく紹介しましたが、もちろんスレッド終了の本質も紹介しています。スレッドはスレッド待機をより適切に完了できるため、スレッド間の秩序ある連携と対話が実現され、プログラムの実行効率と同時実行パフォーマンスが向上します。したがって、スレッドの終了については、スレッド待機と連携するためにのみ使用されます。前回のブログで、対応するインターフェイスを紹介したときに、スレッド待機プロセスがわかりました。ただ愚かに待機しているだけではなく、対応するスレッド終了結果を取得できます。 (pthread_join()インターフェイスの詳細説明) では、スレッド待機とスレッド終了を確認した後、スレッド作成に関連する問題について詳しく説明します。前回のブログでは、スレッド作成に対応するインターフェイスのパラメータを詳細に紹介しただけで、スレッド作成に関する詳細な紹介はありません。次のコードに示すように、その使用法をさらに紹介しましょう。

ここに画像の説明を挿入
このとき、上図を見ると、スレッドを作成する際に、通常の変数や配列変数を提供するだけでなく、クラスオブジェクトも提供できることがわかります。このクラスには、次のような特定の内容を記述することができます。スレッド名、スレッドID、スレッド作成時刻、スレッド状態、スレッド機能など!次に、オブジェクトを他の関数インターフェイスで使用できるようにアドレス空間を申請し、最終的に対応するインターフェイス (スレッド タスク) で対応するコンテンツを取得します。もちろん、この点を理解した上で、次の図に示すように、スレッドが終了するときに結果を返すことで、対応する情報をメインスレッドに返すこともできます。

ここに画像の説明を挿入
上図によると、この時点で、スレッドを作成するときだけでなく、スレッド実行インターフェイスにパラメータを渡すときにもクラス オブジェクトを渡すことができ、スレッドが終了して終了結果を返すときにもクラス オブジェクトを渡すことができることがわかります。クラスオブジェクトの形式を使用しますが、本質はインターフェイスを設計するときに対応するパラメータがポインタ型である必要があるためですが、この時点でスレッドの作成時とスレッド終了時の詳細な使用法を理解でき、次のことを理解できます。異なるスレッドが実行され、異なるコードが実行されます。セクション、つまり、異なるインターフェイスを実行し、異なるタスクを完了し、異なるパラメータ転送を実行し、異なる終了結果を返すことで、同時操作を実現し、異なるタスクを同時に完了することで、パフォーマンスが大幅に向上します。コード実行の効率化。

糸離れとは

スレッドの作成、スレッドの終了、スレッドの待機に関する上記の知識を取得すると、メインスレッドがスレッドを待機しているときは、メインスレッドが待機 (ブロック) 状態になる必要があることがわかります。プロセス制御では、親プロセスが子プロセスを待つ場合、ブロック待ちとノンブロッキング待ち(ポーリング待ち)の2種類の待ち方があります。メインスレッドの待ちも同様に、これも 2 つの方法であり、1 つはブロック待機、もう 1 つは非ブロック待機です。以前に SIGCHLD シグナルについて学んだとき、それがブロック待機であっても非ブロック待機であっても、親プロセスの効率の低下同様に、メインスレッドが待機しているときも効率の低下が発生し、親プロセスが子プロセスを待機しているときに、子のSIGCHLDこの問題を解決するためのプロセスでは、スレッド関連の知識の中で、どのようにこの問題を解決する必要がありますか? 答えは明らかに、スレッドを分離することです。インターフェイス: pthread_detach、基本的な使用形式:int pthread_detach(pthread_t thread);明らかに、このインターフェイスは非常に使いやすく、スレッドをキャンセルするのと同じように、次のコードに示すように、スレッド ID を伝えるだけです。

ここに画像の説明を挿入

スレッドが切り離されると、それ以上待つことができなくなり ( pthread_join)、それ以外の場合はエラーが報告されることを理解してください。この時点で、スレッドが切り離された場合、作成されたスレッドを待機する必要がないことがわかります。待機したため、メインスレッドがスレッドを待つ必要がないことがわかり、メインスレッドのコード実行効率が向上します。

スレッドライブラリとは何ですか

上記のスレッド分離に関する知識を終えて、現時点でスレッド制御に関する知識はほぼ得られています. もちろん、ロックに関する知識についてはここでは詳しく説明しません. 次に、最初に知っておきましょう。スレッド ライブラリに関する知識を得るには、スレッド ライブラリについて話す前に、システムがスレッドをどのように管理するかという小さな問題から始めますが、この問題は、プロセスと最下層を学習した私たちにとっては大きな問題ではありません。もちろん、これら 2 つの単語「管理」について最初に説明し、次に整理し、具体的にどのように説明するかについて説明します。以前にスレッドの概念を学んだときに、私たちが話した TCP 構造はスレッドの説明です。同様に、私たちはスレッドの説明を学んでいました。関連知識として、FILE 構造はロードされたファイルの説明であり、現時点では、FILE 構造は C ライブラリの重要な構造であり、対応するファイル インターフェイスを見つけるのに役立つことを理解する必要があります。 C ライブラリを使用する場合 ファイル構造のアドレス ( struct file)、特に FILE 構造内に配列ポインターがあり、現時点ではこの配列はポインターの配列を表しており、もちろん、これは通常ファイル記述子と呼ばれるものですテーブルの場合、FILE 構造はポインタに基づいています 配列を見つけて、配列の添字に従って対応する関数ポインタを見つけます、もちろん、このときの関数ポインタは、対応するファイルのアドレスを示します。この点を理解した上で、スレッドについても同様です。Linux システムでは、軽量プロセスの設計により、実際のスレッドは存在しません。これが、今話しているスレッド ライブラリです。もちろん、昨日使用した pthread.h ヘッダー ファイルはスレッド ライブラリの重要な部分であり、C ライブラリと同様にスレッド ライブラリには TCB という構造体も存在することを理解しています。同じ理由で、このポインタを使用すると、TCB を通じて対応するスレッド ID を見つけ、最終的にスレッドのアドレスを見つけて、CPU によって実行できます。

上記の知識を理解すると、LWPとは何か、TCBとは何か、スレッドライブラリとは何か、それらの間の接続と機能についてある程度理解できましたので、このとき、これまでの知識(動的ライブラリと静的ライブラリ)を組み合わせて、どのような種類のライブラリであっても、本質は最初にディスクに保存され、次にメモリに保存され、最後にページ テーブルのマッピング関係に基づいて、対応するアドレスをアドレス空間の共有領域に保存する必要があることを理解します。スレッドに依存してアドレス空間を共有することで、各スレッドが対応するアドレスにアクセスし、対応するデータ(ダイナミック ライブラリ)にアクセスできるようになります。もちろん、この知識の具体的なプロセスを理解することは困難です。本当に理解しておく必要があります。必ず読んでください。ここでは、基礎となるソース コードの実行プロセスについては説明しません。各小さなステップがどのように実行されるかを理解する必要があるだけです。この時点で必要なのは、各ダイナミック ライブラリがスレッド ライブラリを参照し、呼び出されると、次の図に示すようにアドレス空間の共有領域に格納されることだけです。

ここに画像の説明を挿入
したがって、この時点では、スレッド ライブラリの本質は、対応するスレッド TCB と対応する LWP であることがわかります。このとき、スレッド ライブラリを呼び出すと、スレッド内の LWP に従って、対応するスレッドの TCB 構造を見つけることができます。次に、TCB 構造に従って対応するスレッド ID を見つけ、最後にスレッド ID に従って対応するアドレスを見つけます。これにより、CPU はアドレスに従って実行する必要がある対応するコードを見つけることができます。次の図に示すように、このときのスレッド ID はアドレス空間内のスレッドのアドレスを表すことに注意してください。

ここに画像の説明を挿入

スレッドに依存しないスタック構造の深い理解 上の
図によると、この時点でスレッド TCB に存在するのはスレッド ローカル ストレージとスレッド スタックだけではないこともわかります。したがって、この時点では、struct pthreadアドレス空間ではなく、スタックスペースだけ?スタックはすべてのスレッドに共通のアドレス空間ですか? 答えは間違いなく「ノー」です。スレッドの概念に関するこれまでの知識から、すべてのスレッドが独自の独立したスタック構造を持っていることがわかっているため、この時点で 1 つの点を理解する必要があります。つまり、メインスレッドはアドレス内のスタックを使用するということです。 space . セカンダリスレッドはライブラリで提供されるスタックを使用します. このようにしてのみ、独立したスタック構造を持ちながら相互に影響を及ぼさないことができます. もちろん、このとき、ライブラリは共有領域に配置されているため、次のことを理解する必要があります、ライブラリ内のスタックの本質は依然としてアドレスにあり、スペースの点では共有領域にのみ存在します。さらに詳しく説明すると、アドレス空間は基本的にメモリに格納されるため、最終的にはスレッドが使用するメモリの別の場所に対応する空間が適用されます。この知識ポイントを理解した後、現時点で「Linux システムでは軽量プロセスを使用していないのではないか? システムの軽量プロセスを作成するプロセスで、対応するスタック構造を取得するにはどうすればよいですか? ?」という疑問が生じるでしょう。それでは次に、軽量プロセスを作成するためのシステム コール インターフェイスについて話しましょう: clone. もちろん、スレッド ライブラリで使用する pthread_create インターフェイスは、スレッドからの軽量プロセスの概念を実現するために、クローン システム インターフェイスをカプセル化したものです。変換、クローン インターフェイスの基本的な使用法:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);このインターフェイスについては詳しく説明しません。最初のパラメーターは軽量プロセスが実行する必要がある関数インターフェイスを表し、2 番目のパラメーターは上記のものを表すことだけを理解する必要がありますchild_stack。前述したように、ライブラリで提供されるスタック スペースは問題ありません。つまり、pthread_create インターフェイスでは、システム コール インターフェイス クローンを呼び出すためにスタック スペースを開き、そのスタック スペースをパラメータの形式で軽量プロセスを実行するため、各セカンダリ スレッドはライブラリによって提供されるスタック スペースを使用します。

スレッドローカルストレージを理解する方法
上図と同様に、スレッドTCBでは、対応するスレッドの属性情報やスタック領域に加えて、各スレッドがスレッドローカルストレージを持っていますが、スレッドローカルストレージとは一体何でしょうか?さて、この時のスレッドのスタック領域の話ですが、関数を実行するスレッドを作成する際には、スレッド作成インターフェース(pthread_create)に、対応する関数のアドレス(関数名)をパラメータとして渡す必要があります。 ) 、スレッド作成インターフェイスがこのアドレスを受け取ると、クローン インターフェイスを呼び出す必要があるため、クローン インターフェイスが使用するスタック領域を開く必要があります。このとき、作成されたスレッドは、このスタック領域を使用して、関数のアドレス (関数パラメーター) を実行し、関数内の一時変数と一時データを保存できます。最後に、スレッドはパラメーターの形式を通じて実行されるインターフェイスのアドレスを取得し、一時的なインターフェイスのアドレスを見つけることもできます。スレッド変数および一時データに対応するスタック空間内の対応する関数インターフェイス。したがって、この時点で、複数のスレッドが同じコード セグメント内で関数インターフェイスを実行する場合、各スレッドは作成時に関数インターフェイスのアドレスを受け取り、独自の独立したスタック スペースを持ち、これらのスタック スペースはインターフェイスに対応する一時変数と一時データを保存します。具体的には、関数の一時変数とデータをスレッド自身の独立したスタックにコピーする方法については、ここでは詳しく説明しません。マルチスレッドであるかどうかだけを理解する必要があります。同じ関数インターフェイス、または複数のスレッドがまったく異なる関数インターフェイスを実行します。すべて、対応する関数インターフェイスのアドレスを対応するスレッドに直接渡し、関数インターフェイスに対応するスタック領域上の変数とデータをスレッドにコピーします。独立したスタックなので、独自の独立したスタック空間を持つスレッドなどの実行フローの場合、アドレス空間のスタック領域上のデータは一般に複数のコピーがあり、各スレッドのスタック空間が存在する可能性があることを最終的に理解しました。初期化されたデータ領域や初期化されていないデータ領域などのグローバル変数を格納する構造体。その中のデータは一意であり、すべてのスレッドで共有されます。これを理解すると、スレッド ローカル ストレージを取得するのは飲み水のようなものです。つまり、グローバル変数をスタック上のローカル変数と同様に作成するには、各スレッドにスタック領域のプライベート コピーを持たせます。この時点では、スレッド ローカル ストレージの概念が導入され、その目的はグローバル変数をプライベート化 (非共有) にすることです。もちろん、本質はスレッドがグローバル変数に直接アクセスせず、独自のローカル ストレージ内のアドレスを申請できるようにすることです。それを保管するために。最後の質問は、スレッドローカル ストレージをどのように実装するかです。非常に簡単で、グローバル変数の前に追加するだけです__threadわかりました、つまり__thread int g_val = 10;

スレッドのミューテックスと同期

スレッド ライブラリに関する上記の知識をすべて終えた後、スレッド関連の学習で最も重要な概念であるスレッドの同期と相互排除を正式に開始します。 CPU の実行プロセスを分析すると、CPU が 1 つしかない場合、スレッドはタイム スライスを通じて順番に実行する必要があるため、同時に実行できないことがわかります。スレッドが真に実行できるのは、スレッドがマルチコア プロセッサの下にある場合だけです。したがって、スレッドが同時に実行されると、この時点で多くの問題に直面することになります。もちろん、問題の本質は、スレッドがリソースを共有することによって引き起こされます。つまり、1 つのプロセスの下にある 2 つの同一のプロセスが、同じリソースにアクセスする場合です。同時にプロセスのアドレス空間を変更し、変更操作(競合状態)がある場合、その時点でプログラムに問題が発生します。この問題を解決するには、スレッドの同時実行と合わせて、ロック、アトミック性、クリティカルリソース、クリティカルセクションなどの概念が登場!ここではクリティカルリソースとは何か、クリティカルセクションとは何か、アトミックオペレーションとは何か、ロックとは何かについて簡単に紹介します まずクリティカルセクションとクリティカルリソースとは対応する概念です クリティカルリソースとは共有リソースとクリティカル領域にも同じことが当てはまります。そのため、共有リソースをクリティカル リソースにするには、ロックまたはその他の同期メカニズム (アトミック操作、セマフォ、ミューテックス) を使用して、ロックまたはアトミック操作の本質は、共有リソースを重要なリソースにすることです。 注: アトミック操作の本質は、不可分性と原子性を備えた分割できない操作です。つまり、スレッドがアクセスするときに、共有リソースは、完全に正常に実行されるか、まったく実行されません。これにより、競合状態の発生を回避することもできます。

スレッドミューテックス

上記の知識を理解した後、スレッドの相互排除とは何かについて話しましょう。もちろん、コード操作を使用して、スレッドを完全な相互排除にし、リソースを共有リソースからクリティカル リソースに変更する方法を確認します。ブロックの知識は比較的面倒な知識であり、習得するのはそれほど簡単ではありません。ここではまず、スレッド ロック、ロックの定義、ロックの初期化、lock pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);unlock pthread_mutex_lock(&mutex);pthread_mutex_unlock(&mutex);およびpthread_mutex_destroy(&mutex);lock destroyに関連するいくつかのインターフェイスについて説明します。これらのインターフェイスを理解した後、次のコードに示すように、共有リソースをロックして重要なリソースにする方法を見てみましょう。

ここに画像の説明を挿入

このとき、上記のコードで共有リソースをロックすることで、共有リソースが競合状態にならないようにしっかりと保護することができますが、この時点でもまだ問題があります。それは、定義したロック自体がまた、グローバル変数 (共有リソース) の場合、ロックを保護できず、ロックの競合状態が発生します。これにより、スレッドがロックを完了し、対応する重要なリソースにアクセスしようとしているときに、突然ロックが解除される可能性があります。別のスレッドによってブロックされる 結合されていない場合も同様に、最終的にコードに問題が発生するため、上記のコードを改善する必要があることはもちろん、スレッドの相互排他に関する知識をさらに理解する必要があります。時間の都合により、このブログはここで終了します。 、次のブログでは、スレッド間の相互排他と同期についてさらに理解しましょう。

まとめ: スレッドの相互排除と同期は、スレッド関連の知識の中で最も重要なポイントです。次回のブログで詳しく説明しましょう。またね。

おすすめ

転載: blog.csdn.net/weixin_74004489/article/details/131709250