この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コアの能力を最大限に活用できます。との間EnterCriticalSection
でLeaveCriticalSection
は、1つのスレッドのみが実行されます。他のスレッドはEnterCriticalSection
呼び出しを待機します...したがって、クリティカルセクションが短いほど、アプリケーションは高速になります。誤って設計されたマルチスレッドアプリケーションの中には、実際にはモノスレッドアプリよりも遅いものがあります。
try ... finally LeaveCriticalSection() end;
また、クリティカルセクション内のコードで例外が発生する可能性がある場合は、ロック解除を保護し、アプリケーションのデッドロックを防ぐために、常に明示的なブロックを作成する必要があることを忘れないでください。
共有データをロック、つまりクリティカルセクションで保護する場合、Delphiは完全にスレッドセーフです。RTL関数内にLOCKがある場合でも、参照カウントされた変数(文字列など)も保護する必要があることに注意してください。このLOCKは、正しい参照カウントを想定し、メモリリークを回避するためにありますが、スレッドセーフではありません。 。できるだけ速くするには、このSOの質問を参照してください。
InterlockExchange
およびの目的はInterlockCompareExchange
、共有ポインター変数の値を変更することです。ポインタ値にアクセスするためのクリティカルセクションの「ライト」バージョンとして表示できます。
いずれの場合も、動作するマルチスレッドコードを作成するのは簡単ではありません。デルファイの専門家がブログに書いたように、それはさらに困難です。
共有データをまったく含まない単純なスレッドを作成するか(スレッドの開始前にデータのプライベートコピーを作成するか、読み取り専用の共有データを使用します-本質的にスレッドセーフです)、または適切に設計され、実績のあるライブラリを呼び出す必要があります--http://otl.17slon.comのように-デバッグ時間を大幅に節約できます。