共有リソースの場合、マルチスレッド環境ではロックがないと、ロックが解除される可能性が非常に高くなります。
競争と協力
シングルコア CPU システムでは、複数のプログラムが同時に実行されているような錯覚を実現するために、通常、オペレーティング システムはタイム スライス スケジューリングを使用して、各プロセスが一度にタイム スライスを実行できるようにします。このタイムスライスの時間は非常に短いため、現象同時実行
さらに、オペレーティング システムは、プロセスごとに巨大なプライベート仮想メモリのような錯覚を生み出します。このアドレス空間の抽象化により、各プログラムが独自のメモリを持っているように見えますが、実際には、オペレーティング システムは密かに複数のアドレス空間を物理的に多重化します。メモリとかディスクとか。
プログラムに実行フローが 1 つしかない場合、それはシングルスレッドであることも意味します。もちろん、プログラムは複数の実行プロセスを持つことができ、これはいわゆるマルチスレッド プログラムであり、スレッドはスケジューリングの基本単位であり、プロセスはリソース割り当ての基本単位です。
したがって、スレッドはコード セグメント、ヒープ領域、データ セグメント、オープン ファイル、その他のリソースなどのプロセス リソースを共有できますが、各スレッドは独自の独立したスタック領域を持ちます。
ここで問題となるのは、複数のスレッドが共有リソースを奪い合う場合、有効な対策を講じないと、共有データの混乱を引き起こすことです。
実験: 次のコードに示すように、2 つのスレッドを作成し、それぞれ共有変数iを1ずつインクリメントする処理を10,000回実行します。
論理的に言えば、変数 i は最終的に20000になるはずですが、実際の結果は次のようになります。
2 回実行すると、i の値が 20000 またはその他の結果になることがわかります。
実行ごとにエラーが発生するだけでなく、異なる結果も得られます。コンピュータでは許されないことですが、確率の低いエラーではありますが、確率的には必ず起こります。
なぜこのようなことが起こるのでしょうか?
なぜこれが起こるのかを理解するには、カウンター i 変数を更新するためにコンパイラーによって生成されたコード シーケンス、つまりアセンブリ命令が実行される順序を理解する必要があります。
この例では、数値 1 を i に追加するだけなので、対応するアセンブリ命令の実行プロセスは次のようになります。
数値1を追加するだけで、CPU の実行中に実際に命令が実行されることが わかります。 i
3
スレッド 1 がこのコード領域に入ると、スレッド 1 は i の値 (この時点では 50 であると仮定します) をメモリからレジスタにロードし、その後レジスタに 1 を加算します。この時点でのレジスタ内の i の値は 51 です。時間。
ここで、残念なことが起こります。クロック割り込みが発生します。したがって、オペレーティング システムは、現在実行中のスレッドの状態をスレッドのスレッド制御ブロック TCB に保存します。
ここで、さらに悪いことが起こります。スレッド 2 が実行されるようにスケジュールされており、同じコードを入力します。また、最初の命令を実行し、メモリから i の値をフェッチしてレジスタに置きます。この時点では、メモリ内の i の値はまだ 50 なので、スレッド 2 のレジスタの i の値も 50 になります。スレッド 2 が次の 2 命令を実行すると仮定すると、レジスタの i の値は +1 になり、レジスタの i の値はメモリに保存されるため、このときのグローバル変数 i の値は 51 になります。
最後に、別のコンテキストの切り替えが発生し、スレッド 1 が実行を再開します。2 つのアセンブリ命令が実行され、次のアセンブリ命令を実行する準備ができていることに注意してください。以前、スレッド 1 レジスタの i の値は 51 であったため、最後の命令を実行して値をメモリに保存した後、グローバル変数 i の値は再び 51 に設定されます。
簡単に言うと、i (値 50) を増やすコードが 2 回実行されます。論理的には、最終的な i 値は 52 になるはずですが、制御不能なスケジューリングにより、最終的な i 値は 51 になります。
スレッド 1 と 2 上のスレッド 2 の実行プロセスは、次のように表現できます。
相互排他的
上に示した状況は競合状態と呼ばれます。複数のスレッドが共有変数を操作するために互いに競合します。不運により、つまり、実行中にコンテキストの切り替えが発生すると、間違った結果が得られます。実際、実行ごとに異なる結果が得られる可能性があります。したがって、出力結果には不確実性があります。
共有変数を操作するこのコードをマルチスレッドで実行すると競合状態が発生する可能性があるため、このコードをクリティカル セクションと呼びます。これは、共有リソースにアクセスするコードの断片であり、複数のスレッドで同時に実行してはなりません。
このコードが相互に排他的であることを望みます。つまり、スレッドがクリティカル セクションで実行されるとき、他のスレッドがクリティカル セクションに入ることを防止する必要があります。つまり、このコードの実行中に表示できるスレッドは最大でも 1 つだけです。コード。
さらに、相互排他はマルチスレッドのみに適用されるものではありません。複数のスレッドが共有リソースをめぐって競合する場合、相互排他を使用して、リソースの競合によって引き起こされるリソースの混乱を回避することもできます。
同期する
相互排他は、同時プロセス/スレッドによるクリティカル セクションの使用の問題を解決します。クリティカル セクション制御に基づくこの対話は比較的単純で、プロセス/スレッドがクリティカル セクションに参加している限り、最初のプロセス/スレッドがクリティカル セクションを離れるまで、クリティカル セクションに入ろうとする他のプロセス/スレッドはブロックされます。
マルチスレッドでは、各スレッドは必ずしも順番に実行されるわけではなく、基本的には独立した予測不可能な速度で進みますが、場合によっては、共通の目標を達成するために複数のスレッドが緊密に連携できることを期待します。
たとえば、スレッド 1 はデータの読み取りを担当し、スレッド 2 はデータの処理を担当し、これら 2 つのスレッドは連携し、相互に依存します。スレッド 2 がスレッド 1 からウェイクアップ通知を受信しない場合、スレッド 2 は常にブロックして待機します。スレッド 1 がデータの読み取りを完了し、データをスレッド 2 に渡す必要がある場合、スレッド 1 はスレッド 2 をウェイクアップして、スレッド 2 をウェイクアップ通知に渡します。データを処理のためにスレッド 2 に送信します。
いわゆる同期とは、並行プロセス/スレッドがいくつかの重要なポイントで相互に待機し、通信する必要があることを意味します。この相互に制限された情報の待機と通信は、プロセス/スレッドの同期と呼ばれます。
PS: 同期と相互排他は 2 つの異なる概念です。
- 同期: [操作 A は操作 B の前に実行する必要がある]、[操作 C は操作 A と操作 B の両方が完了した後に実行する必要がある] など。
- 相互排他: [操作 A と操作 B は同時に実行できません];
相互排他と同期の実装と使用
プロセス/スレッドの同時実行の過程では、プロセス/スレッド間に排他や同期などの協力関係が存在します。
プロセス/スレッド間の正しい連携を実現するには、オペレーティング システムがプロセスの連携を実現するための手段と方法を提供する必要があります。主な方法は次の 2 つです。
- ロック: ロックとロック解除の操作
- セマフォ:P、V動作
どちらもプロセス/スレッドの相互排他を簡単に実現でき、セマフォはロックよりも強力で、プロセス/スレッドの同期も簡単に実現できます。
ロック
ロック操作とロック解除操作を使用すると、同時スレッド/プロセスの相互排他問題を解決できます。
クリティカル セクションに入ろうとするスレッドは、まずロック操作を実行する必要があります。ロック操作が成功すると、スレッドはクリティカル領域に入ることができ、クリティカル リソースへのアクセスが完了した後、ロック解除操作が実行されてクリティカル リソースが解放されます。
ロックの実装により、「ビジー待機ロック」と「ビジー待機なしロック」に分けられます。
ビジーウェイトロックの実装
ビジー待機ロックの実装を説明する前に、最新の CPU アーキテクチャによって提供される特別なアトミック操作命令であるTest-and-Set 命令を理解する必要があります。
テスト アンド セット命令を C コードで表現すると、次の形式になります。
test および set ディレクティブは次のことを行います。
- old_ptr を新しい値 new に更新します。
- old_ptr の古い値を返します。
重要なのは、これらのコードがアトミックに実行されるということです。古い値をテストして新しい値を設定できるため、この命令を [テスト アンド セット] と呼びます。
アトミック操作: すべてが実行されるか、まったく実行されないかのどちらかであり、半分が実行された中間状態は存在できません。
Test-and-Set コマンドを使用して [ビジー待機ロック] を実現できます。コードは次のとおりです。
このロックが機能する理由を理解してみましょう。
- シナリオ 1: まず、スレッドが実行中でlock()を呼び出していると仮定します。他のスレッドがロックを保持していないため、flagは 0 です。TestAndSet(flag, 1)メソッドが呼び出され、0 が返されると、スレッドは while ループから抜け出してロックを取得します。同時に、フラグはアトミックに 1 に設定され、ロックがすでに保持されていることを示します。スレッドがクリティカル セクションを離れると、unlock()を呼び出してフラグを 0 にクリアします。
- シナリオ 2: スレッドがすでにロックを保持している場合 (つまり、フラグが 1 である場合)。このスレッドはlock()を呼び出し、次に TestAndSet(flag, 1) を呼び出します。今回は1が返されます。別のスレッドがロックを保持している限り、TestAndSet() は繰り返し 1 を返し、このスレッドは待機中になります。最終的にフラグが 0 に変更されると、スレッドはTestAndSet()を呼び出し、0 を返し、それをアトミックに 1 に設定します。これにより、ロックが取得され、クリティカル セクションに入ります。
明らかに、ロックを取得できない場合、スレッドは何もせずに常にループするため、 [ビジー待機ロック] と呼ばれ、スピン ロックとも呼ばれます。
これは最も単純な種類のロックで、ロックが使用可能になるまで CPU サイクルを使用して常に回転します。シングル プロセッサでは、プリエンプティブ スケジューラが必要です (つまり、1 つのスレッドが他のスレッドを実行するためにクロックによって常に中断されます)。それ以外の場合、スピン ロックは単一の CPU で使用できません。これは、回転しているスレッドが CPU を決して放棄しないためです。 。
ロックを待たずに実装する
待機ロックなし: ロックを取得できない場合、スピンは必要ありません。ロックが取得できない場合は、現在のスレッドをロック待ちキューに入れてからスケジューラを実行し、CPUを他のスレッドで実行させる
今回は、単純なロックの実装を 2 つだけ提案します。もちろん、特定のオペレーティング システムではさらに複雑になりますが、この例の 2 つの基本要素から切り離すこともできません。
信号量
セマフォは、共有リソースへのアクセスを調整するためにオペレーティング システムによって提供される方法です。
通常、セマフォはリソースの数を表し、対応する変数は整数 (sem) 変数です。
さらに、信号を制御するための 2 つのアトミック操作システム コール関数があります。
- P 操作: semを1 だけデクリメントします。減算後、sem < 0の場合、プロセス/スレッドはブロッキング待機に入ります。そうでない場合は続行し、P 操作がブロックされる可能性があることを示します。
- V 操作: semに1を追加します。追加後、 sem <= 0 の場合、待機中のプロセス/スレッドをウェイクアップし、V 操作がブロックされないことを示します。
ヒント V 演算で sem <= 0 になるのはなぜですか
例: sem = 1 の場合、P 操作を実行するスレッドが 3 つあります。
- 最初のスレッド P が動作した後、sem = 0、最初のスレッドは実行を継続します。
- 2 番目のスレッド P の操作後、sem = -1; sem < 0 2 番目のスレッドはブロックして待機します
- 3 番目のスレッド P が動作した後、 sem = -2; sem < 0 3 番目のスレッドはブロックされて待機します
このとき、最初のスレッドが V オペレーションを実行した後、 sem=-1; sem <=0 となるため、ブロックされて待機している 2 番目または 3 番目のスレッドを起こす必要があります。
P 操作はクリティカル エリアに入る前、V 操作はクリティカル エリアから出た後であり、これら 2 つの操作はペアで指定する必要があります。
例えると、2 つのリソースのセマフォは 2 つの電車の線路に相当し、PV の動作プロセスは次のようになります。
オペレーティングシステムはどのようにして太陽光発電の運用を実現しているのでしょうか?
セマフォのデータ構造と PV 動作のアルゴリズムは次のように説明されます。
PV 操作の機能はオペレーティング システムによって管理および実装されるため、オペレーティング システムは PV 機能の実行をアトミックにします。
PV操作はどのように使用されますか?
セマフォはクリティカルセクションの相互排他アクセス制御を実現するだけでなく、スレッド間のイベントの同期も実現します。
セマフォによりクリティカルセクションの相互排他アクセスを実現
セマフォs は共有リソースのタイプごとに設定され、その初期値は1で、クリティカルなリソースが占有されていないことを示します。
クリティカル セクションに入る操作がP(s)とV(s)の間に配置されている限り、プロセス/スレッドの相互排他を実現できます。
この時点で、クリティカル領域に入ろうとするスレッドは、最初にミューテックス セマフォに対して P 操作を実行し、次にクリティカル リソースへのアクセスが完了した後に V 操作を実行する必要があります。ミューテックス セマフォの初期値は 1 であるため、最初のスレッドが P 操作を実行した後、s の値は 0 になり、クリティカル リソースが空いており、クリティカル セクションに入るためにスレッドに割り当てることができることを示します。
この時点で 2 番目のスレッドがクリティカル セクションに入りたい場合は、最初に P 操作を実行する必要があり、その結果 s が負の値になり、これはクリティカル リソースが占有されていることを意味するため、2 番目のスレッドはブロックされます。
そして、最初のスレッドが V 操作を実行するまで、クリティカル リソースを解放して s の値を 0 に戻し、次に 2 番目のスレッドをウェイクアップしてクリティカル セクションに入らせ、クリティカル リソースへのアクセスが終了した後、V を実行します。再度演算するとsは初期値1に戻ります。
2 つの同時スレッドの場合、ミューテックス セマフォの値は 1、0、および -1 の 3 つの値のみを取り、それぞれ次のことを表します。
- ミューテックス セマフォが 1 の場合、スレッドがクリティカル セクションに入らないことを意味します
- ミューテックス セマフォが 0 の場合、スレッドがクリティカル セクションに入ったことを意味します。
- ミューテックス セマフォが -1 の場合、1 つのスレッドがクリティカル セクションに入り、別のスレッドがそのセクションに入るのを待っていることを意味します。
相互排除セマフォにより、常に 1 つのスレッドだけがクリティカル セクションで実行されることが保証され、相互排除の効果が実現されます。
セマフォはイベント同期を実装します
同期する方法は、初期値が0 のセマフォを設定することです。
母親が最初に息子に料理をするかどうか尋ねたとき、その実行は息子に食事が必要かどうかを尋ねたのと同じであり、初期値は 0 だったので、この時点で -1 になり、息子が食事する必要がないことを示します。食べるので、母スレッドは待機状態に入りました。 P(s1)
s1
s1
息子がお腹が空くと、息子が実行され、セマフォが -1 から 0 に変化します。これは、息子がこの時点で食事をする必要があることを示し、ブロックされていたマザー スレッドが目覚め、マザー スレッドがクックを開始します。 V(s1)
s1
次に、息子スレッドが実行されますが、これは母親に食事が終わったかどうかを尋ねることに相当します。 初期値は 0 なので、 この時点では -1 になり、母親が料理を終えていないことを示し、息子スレッドは待っている。 P(s2)
s2
s2
最後に、母親はついに料理を終えたのでそれを実行し、セマフォが -1 から 0 に戻ったので、待っていた息子スレッドを目覚めさせました。目覚めた後、息子スレッドは食事を開始できます V(s2)
s2
生産者と消費者の問題
生産者と消費者の問題の説明:
- プロデューサがデータを生成した後、データはバッファに配置されます。
- コンシューマは処理のためにバッファからデータをフェッチします
- 常に1 つのプロデューサーまたはコンシューマーのみがバッファーにアクセスできます。
問題の分析から、次のことが結論付けられます。
- バッファを操作できるスレッドは常に 1 つだけです。これは、バッファの操作が相互排他を必要とする重要なコードであることを示しています。
- バッファが空の場合、コンシューマはプロデューサがデータを生成するのを待つ必要があり、バッファがいっぱいの場合、プロデューサはコンシューマがデータをフェッチするのを待つ必要があります。プロデューサとコンシューマは同期する必要があることを説明します。
次に、次の 3 つのセマフォが必要になります。
- 相互排他セマフォmutex : バッファへの相互排他的アクセスに使用され、初期値は 1 です。
- リソース セマフォfullBuffers : コンシューマーがバッファーにデータがあるかどうかを尋ね、データがある場合はデータを読み取るために使用されます。初期値は 0 (バッファーが最初は空であることを示します)。
- リソース セマフォemptyBuffers : プロデューサーがバッファーにスペースがあるかどうかを問い合わせるために使用され、スペースがある場合はデータが生成され、初期化値は n (バッファー サイズ) です。
具体的な実装コード:
コンシューマー スレッドが最初にP(fullBuffers)を実行すると、セマフォfullBuffersの初期値は0 であるため、この時点でfullBuffersの値は0 から -1 に変化します。これは、バッファーにデータがないことを示し、消費者は待つことしかできません。
次に、プロデューサがP(fullBuffers)を実行する番です。これは、空のスロットを 1 つ減らすことを意味します。現在クリティカル セクションのコードを実行している他のプロデューサ スレッドがない場合、プロデューサ スレッドはデータをバッファに入れることができます。 、V(fullBuffers)を実行すると、セマフォfullBuffers が-1 から 0 に変化します。これは、コンシューマ スレッドがブロックされてデータを待機していることを示し、ブロックされ待機中のコンシューマ スレッドが起動されます。
コンシューマ スレッドがウェイクアップされた後、その時点で他のコンシューマ スレッドがデータを読み取っていない場合、コンシューマ スレッドは直接クリティカル セクションに入り、バッファからデータを読み取ることができます。最後に、クリティカル セクションを離れ、空のスロットの数に 1 を加えます。
古典的な同期の問題
食事の哲学者の問題
哲学者の問題ステートメント:
- 円卓を囲んでヌードルを食べる5人の哲学者
- このテーブルにはフォークが 5 つしかなく、2 人の哲学者ごとに 1 つのフォークが置かれます
- 哲学者は一緒に考えていないので、考えている間にお腹が空くと何かを食べたくなります。
- しかし、これらの哲学者たちは、麺類を食べるためには 2 本のフォークが必要です。つまり、食べるためには左右のフォークが必要です。
- 食べたらフォークを元の位置に戻す
質問: 誰もフォークを決して取得しないように、哲学者の行動が秩序ある方法で実行されることを保証するにはどうすればよいですか?
オプション 1
セマフォ手法、つまり PV 操作を使用して次のことを解決します。
上記の手順は非常に自然に思えます。フォークを手に取り、P を使用して操作します。つまり、フォークがある場合はそれを直接使用し、フォークがない場合は他の哲学者がフォークを戻すのを待ちます。
ただし、この解決策には極端な問題があります。5人の哲学者が同時に左側のフォークを拾ったと仮定すると、テーブル上にはフォークがなくなり、誰も右側のフォークを手に入れることができなくなります。ステートメント P(fork[(i+1)%N]) はブロックされており、デッドロックが発生していることは明らかです。
オプション II
プラン 1 では、同時に左のフォークが競合してデッドロックが発生するため、フォークを取得する前にミューテックス セマフォを追加します。コードは次のとおりです。
上記のプログラムにおける相互排他セマフォの機能は、哲学者がクリティカルエリアに入っている限り、つまりフォークを取ろうとしている間、他の哲学者は動けないということです。
解決策 2 では、哲学者が順番に食事をすることができますが、一度に食べることができるのは 1 人の哲学者だけであり、テーブルには 5 本のフォークがあります。2 人の哲学者が同時に食べることができるのは合理的であるため、効率の観点からすると、最良の解決策ではありません。
3番目の解決策
解決策 1 の問題点は、すべての哲学者が同時に左のナイフとフォークを持てる可能性があるため、哲学者が同時に左のナイフとフォークを持たないようにして、分岐構造を採用し、哲学者の数に応じて異なるアクションを実行します。
つまり、偶数番号の哲学者は [最初に左フォークを取り、次に右フォークを取る] とし、奇数番号の哲学者は [最初に右フォークを取り、次に左フォークを取る] とします。
上記プログラムでは、Pを操作する際、哲学者の番号に応じて左右のフォークを取る順番が異なります。さらに、V 操作はブロックしないため、V 操作は分岐する必要がありません。
オプション 3 は、デッドロックがなく、2 人が同時に食事をできることを意味します。
オプション 4
これは、配列状態を使用して各哲学者の 3 つの状態 (食事状態、思考状態、空腹状態 (フォークを取ろうとしている)) を記録する別の実現可能な解決策です。
その場合、哲学者は、隣り合う人が両方とも食事をしていない場合にのみ、食事状態に入ることができます。
i 番目の哲学者の近傍は、マクロ LEFT および RIGHT によって定義されます。
- 左:(i+5-1)%5
- 右:(i+1) % 5
たとえば、i が 2 の場合、LEFT は 1、RIGHT は 3 になります。
具体的な実装コードは以下のとおりです。
上記のプログラムは、各哲学者に 1 つずつセマフォの配列を使用するため、必要なフォークが占有されている間、食事をしたい哲学者はブロックされます。
各プロセス/スレッドは smart_person
メイン コードとして関数を実行しますが、他のプロセス /スレッド は 単なる通常の関数であり、別個のプロセス/スレッドではないことに注意してください。take_forks、
put_forks
test
リーダーライター問題
前述のダイニング哲学者問題は、I/O デバイスなどの排他的アクセスが制限された競合する問題などのプロセスをモデル化するのに役立ちます。
さらに、データベース アクセスのモデルを確立するよく知られた「リーダー/ライター」問題があります。
リーダーはデータの読み取りのみが可能で、データの変更はできませんが、ライターはデータの読み取りまたは変更が可能です。
リーダーライターの問題の説明:
- 「読み取り-読み取り」では、次のことが可能になります。同時に、複数のリーダーが同時に読み取りを許可されます。
- 「読み書き」相互排他: ライターがいない場合でもリーダーは読み取ることができ、リーダーがいない場合はライターは書き込むことができます。
- 「書き込み-書き込み」相互排他: ライターは、他のライターが存在しない場合にのみ書き込みを行うことができます。
オプション 1
セマフォを使用して問題を解決します。
- セマフォ wMutex: 書き込み操作を制御する相互排他セマフォ。初期値は 1。
- リーダー数 rCount: 読んでいるリーダーの数。0 に初期化されます。
- セマフォ rCountMutex: rCount リーダー カウンタ ポイントの相互排他的な変更を制御します。初期値は 1 です。
コードの実装:
上記の実装は読者ファースト戦略であり、読んでいる読者がいる限り後続の読者が直接エントリーできるため、読者がエントリーし続けると書き手は飢餓状態に陥ります。
オプション II
読者第一の戦略があるので、当然、作家第一の戦略も存在します。
- ライターが書き込みの準備ができている限り、ライターはできるだけ早く書き込み操作を実行し、後続のリーダーはブロックする必要があります。
- 書き続ける作家がいると読者は飢える
スキーム 1 に基づいて次の変数を追加します。
- セマフォrMutex : リーダーの入力を制御する相互排他セマフォ。初期値は 1 です。
- セマフォwDataMutex : ライターの書き込み操作を制御する相互排他セマフォ。初期値は 1 です。
- ライター数wCount : ライターの数を記録します。初期値は 0 です。
- セマフォwCountMutex : wCount 相互排他変更を制御します。初期値は 1 です。
実装コードは次のとおりです。
ここでのrMutexの役割 は、最初にデータを読み取っている複数のリーダーがあり、それらがすべてリーダー キューに入るということです。このとき、ライターが来ます。P(rMutex)を実行すると、後続のリーダーはブロックされて入れなくなります。 on rMutex リーダーキュー、ライターが到着すると、すべてのリーダーがライターキューに入ることができるため、ライターの優先順位が保証されます。
同時に、最初のライターがP(rMutex)を実行した後、すぐに書き込みを開始することはできません。リーダー キューに入るすべてのリーダーが読み取り操作を完了するまで待機し、V(wDataMutex)を通じてライターの書き込み操作をウェイクアップする必要があります。
3番目の解決策
読者ファースト戦略もライターファースト戦略も飢餓を引き起こすので、公平な戦略を実行しましょう。
公正な戦略:
- 同じ優先順位。
- ライターとリーダーは相互に排他的にアクセスします。
- クリティカル セクションにアクセスできるのは 1 人のライターだけです。
- 複数のリーダーが重要なリソースに同時にアクセスできます。
特定のコード実装:
スキーム 1 のリーダーファースト戦略を比較すると、リーダーファースト戦略では、後続のリーダーが到着する限り、リーダーはリーダーのキューに入ることができますが、ライターはリーダーが到着しなくなるまで待機する必要があることがわかります。
リーダーが到着しない場合、リーダー キューは空になります。つまり、rCount = 0になります。この時点で、ライターはクリティカル セクションに入って書き込み操作を実行できます。
ここでのフラグの役割は、リーダーのこの特別な許可を阻止することです (リーダーが到着している限り、リーダーはリーダーのキューに入ることができます)。
例: 一部のリーダーが最初にデータを読み取り、それらはすべてリーダー キューに入りますが、このとき、ライターが来て操作を実行するため、後続のリーダーはブロックされ、リーダー キューに入れなくなり、リーダーは 徐々にキューが空です。これは 0 に減ります。 P(falg)
flag
rCount
ライターはすぐに書き込みを開始できません (この時点ではリーダー キューが空ではないため)、セマフォでブロックされます。リーダー キュー内のすべてのリーダーが読み取りを終了した後、最後のリーダー プロセスが実行され、今ライターがウェイクアップしますそれ以外の場合は、書き込み操作が続行されます。 wDataMutex
V(wDataMutex)