11

マルチプロセッサマシンでのメモリキャッシュの一貫性の問題に対処するMSDNの記事「同期とマルチプロセッサの問題」を読みました。彼らが提供する例では競合状態が発生する可能性があるとは思わなかったので、これは本当に私にとって目を見張るものでした。この記事では、メモリへの書き込みが、私のコードで記述された順序で(他のCPUの観点から)実際には発生しない可能性があることを説明しています。これは私にとって新しいコンセプトです!

この記事では、2つのソリューションを提供します。

  1. 複数のCPU間でキャッシュの一貫性が必要な変数に「volatile」キーワードを使用します。これはC/C ++キーワードであり、Delphiでは使用できません。
  2. InterlockExchange()およびInterlockCompareExchange()を使用します。これは、必要に応じてDelphiで実行できることです。少し面倒なようです。

この記事では、「次の同期関数は、適切なバリアを使用してメモリの順序付けを保証します。•クリティカルセクションに出入りする関数」についても言及しています。

これは私が理解していない部分です。これは、クリティカルセクションを使用する関数に制限されているメモリへの書き込みが、キャッシュの一貫性やメモリの順序の問題の影響を受けないことを意味しますか?Interlock *()関数に反対するものは何もありませんが、ツールベルトに別のツールがあると便利です。

4

2 に答える 2

8

このMSDNの記事は、マルチスレッドアプリケーション開発の最初のステップにすぎません。つまり、「共有変数をロック(クリティカルセクション)で保護することを意味します。これは、読み取り/書き込みするデータがすべてのユーザーで同じであるかどうかわからないためです。スレッド」。

CPUコアごとのキャッシュは、考えられる問題の1つにすぎず、誤った値の読み取りにつながります。競合状態につながる可能性のあるもう1つの問題は、2つのスレッドが同時にリソースに書き込むことです。後でどちらの値が格納されるかを知ることは不可能です。

コードはデータに一貫性があることを期待しているため、一部のマルチスレッドプログラムは正しく動作しない可能性があります。マルチスレッドでは、共有変数を処理するときに、個々の命令を介して記述したコードが期待どおりに実行されるかどうかはわかりません。

InterlockedExchange/InterlockedIncrement関数は、LOCKプレフィックスが付いた低レベルのasmオペコードです(またはXCHG EDX,[EAX]opcodeのように設計によってロックされています)。これにより、すべてのCPUコアのキャッシュコヒーレンシが強制され、asmopcodeの実行がスレッドセーフになります。

たとえば、文字列値を割り当てるときに文字列参照カウントがどのように実装されるかを次に示します(Delphiの元のコードは著作権で保護されているため_LStrAsg、System.pasを参照してください。これはDelphi 7/2002用に最適化されたバージョンのRTLからのものです)。

            MOV     ECX,[EDX-skew].StrRec.refCnt
            INC     ECX   { thread-unsafe increment ECX = reference count }
            JG      @@1   { ECX=-1 -> literal string -> jump not taken }
            .....
       @@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count }
            MOV     ECX,[EAX]   
            ...

INC ECX最初と-の間に違いがありますLOCK INC [EDX-skew].StrRec.refCnt-最初はECXをインクリメントし、参照カウント変数ではないだけでなく、最初はスレッドセーフではありませんが、2番目は接頭辞LOCKが付いているため、スレッドセーフになります。

ちなみに、このLOCKプレフィックスはRTLのマルチスレッドスケーリングの問題の1つです。新しいCPUの方が優れていますが、それでも完全ではありません。

したがって、クリティカルセクションを使用することは、コードをスレッドセーフにする最も簡単な方法です。

var GlobalVariable: string;
    GlobalSection: TRTLCriticalSection;

procedure TThreadOne.Execute;
var LocalVariable: string;
begin
   ...
   EnterCriticalSection(GlobalSection);
   LocalVariable := GlobalVariable+'a'; { modify GlobalVariable }
   GlobalVariable := LocalVariable;
   LeaveCriticalSection(GlobalSection);
   ....
end;

procedure TThreadTwp.Execute;
var LocalVariable: string;
begin
   ...
   EnterCriticalSection(GlobalSection);
   LocalVariable := GlobalVariable; { thread-safe read GlobalVariable }
   LeaveCriticalSection(GlobalSection);
   ....
end;

ローカル変数を使用すると、クリティカルセクションが短くなるため、アプリケーションのスケーリングが向上し、CPUコアの能力を最大限に活用できます。との間EnterCriticalSectionLeaveCriticalSectionは、1つのスレッドのみが実行されます。他のスレッドはEnterCriticalSection呼び出しを待機します...したがって、クリティカルセクションが短いほど、アプリケーションは高速になります。誤って設計されたマルチスレッドアプリケーションの中には、実際にはモノスレッドアプリよりも遅いものがあります。

try ... finally LeaveCriticalSection() end;また、クリティカルセクション内のコードで例外が発生する可能性がある場合は、ロック解除を保護し、アプリケーションのデッドロックを防ぐために、常に明示的なブロックを作成する必要があることを忘れないでください。

共有データをロック、つまりクリティカルセクションで保護する場合、Delphiは完全にスレッドセーフです。RTL関数内にLOCKがある場合でも、参照カウントされた変数(文字列など)も保護する必要があることに注意してください。このLOCKは、正しい参照カウントを想定し、メモリリークを回避するためにありますが、スレッドセーフではありません。 。できるだけ速くするには、このSOの質問を参照してください

InterlockExchangeおよびの目的はInterlockCompareExchange、共有ポインター変数の値を変更することです。ポインタ値にアクセスするためのクリティカルセクションの「ライト」バージョンとして表示できます。

いずれの場合も、動作するマルチスレッドコードを作成するのは簡単ではありません。デルファイの専門家がブログに書いたように、それはさらに困難です。

共有データをまったく含まない単純なスレッドを作成するか(スレッドの開始前にデータのプライベートコピーを作成するか、読み取り専用の共有データを使用します-本質的にスレッドセーフです)、または適切に設計され、実績のあるライブラリを呼び出す必要があります--http://otl.17slon.comのように-デバッグ時間を大幅に節約できます。

于 2011-08-29T06:04:37.327 に答える
7

まず第一に、言語標準によれば、volatileは記事に書かれていることをしません。volatileの取得と解放のセマンティクスはMSVC固有です。これは、他のコンパイラまたは他のプラットフォームでコンパイルする場合に問題になる可能性があります。C ++ 11は、言語でサポートされているアトミック変数を導入します。これにより、やがて、スレッディング構造としてのvolatileの(誤)使用に終止符が打たれることを願っています。

保護された変数の読み取りと書き込みがすべてのスレッドから正しく表示されるように、クリティカルセクションとミューテックスが実際に実装されています。

クリティカルセクションとミューテックス(ロック)を考える最良の方法は、シリアル化を実現するためのデバイスだと思います。つまり、このようなロックで保護されたコードのブロックは、重複することなく次々に連続して実行されます。シリアル化はメモリアクセスにも適用されます。キャッシュの一貫性や読み取り/書き込みの並べ替えによる問題は発生しません。

インターロック機能は、メモリバスのハードウェアベースのロックを使用して実装されます。これらの関数は、ロックフリーアルゴリズムによって使用されます。これが意味するのは、クリティカルセクションのような重いロックを使用するのではなく、これらの軽量のハードウェアロックを使用するということです。

ロックフリーアルゴリズムは、ロックに基づくアルゴリズムよりも効率的ですが、ロックフリーアルゴリズムを正しく記述するのは非常に難しい場合があります。パフォーマンスへの影響が識別できない限り、ロックフリーよりもクリティカルセクションを優先します。

読む価値のあるもう1つの記事は、「二重チェックのロックが壊れている」という宣言です。

于 2011-08-28T20:24:30.030 に答える