原子操作

先日、彼らは私に質問をしました。



また、システムモジュールから_LStrClrプロシージャを呼び出すときに、LOCKプレフィックスまたはそのアナログInterlockedDecrementが必要な理由は何ですか。 このプロシージャは、回線のリンク参照カウンタをデクリメントし、リセットされると、以前に回線が占有していたメモリを解放します。



質問の本質はこれでした。文字列が2つのスレッドから同時にrefを失う状況を想像することはほとんど不可能であるため、この場合のアトミック操作は冗長です。



原則として、前提は興味深いものですが、...



しかし、文字列をスレッドクラスに渡します。

これにより、少なくともrefCntが増加するため、参照カウンターをデクリメントするときにアトミック操作を使用しなかった場合、MemLeakに「到達」できます。



これは、_LStrClrコードを示しています。



procedure _LStrClr(var S); {$IFDEF PUREPASCAL} var P: PStrRec; begin if Pointer(S) <> nil then begin P := Pointer(Integer(S) - Sizeof(StrRec)); Pointer(S) := nil; if P.refCnt > 0 then if InterlockedDecrement(P.refCnt) = 0 then FreeMem(P); end; end; {$ELSE} asm { -> EAX pointer to str } MOV EDX,[EAX] { fetch str } TEST EDX,EDX { if nil, nothing to do } JE @@done MOV dword ptr [EAX],0 { clear str } MOV ECX,[EDX-skew].StrRec.refCnt { fetch refCnt } DEC ECX { if < 0: literal str } JL @@done LOCK DEC [EDX-skew].StrRec.refCnt { threadsafe dec refCount } JNE @@done PUSH EAX LEA EAX,[EDX-skew].StrRec.refCnt { if refCnt now zero, deallocate} CALL _FreeMem POP EAX @@done: end; {$ENDIF}
      
      







非アトミックデクリメントを使用する場合、JNE命令は誤って実行される可能性が非常に高くなります。 (LOCKプレフィックスを削除すると、実際には正しく実行されません)。



もちろん、作業を説明しているIntelマニュアルの例を使用してこの状況を説明しようとしましたが、最終的に次の例を実装することにしました(質問の著者に納得させることができました)。



 program interlocked; {$APPTYPE CONSOLE} uses Windows; const Limit = 1000000; DoubleLimit = Limit shl 1; var SameGlobalVariable: Integer; function Test1(lpParam: Pointer): DWORD; stdcall; var I: Integer; begin for I := 0 to Limit - 1 do asm lea eax, SameGlobalVariable inc [eax] //   end; end; function Test2(lpParam: Pointer): DWORD; stdcall; var I: Integer; begin for I := 0 to Limit - 1 do asm lea eax, SameGlobalVariable lock inc [eax] //   end; end; var I: Integer; hThread: THandle; ThreadID: DWORD; begin //     SameGlobalVariable SameGlobalVariable := 0; hThread := CreateThread(nil, 0, @Test1, nil, 0, ThreadID); for I := 0 to Limit - 1 do asm lea eax, SameGlobalVariable inc [eax] //   end; WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); if SameGlobalVariable <> DoubleLimit then Writeln('Step one failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable); //     SameGlobalVariable SameGlobalVariable := 0; hThread := CreateThread(nil, 0, @Test2, nil, 0, ThreadID); for I := 0 to Limit - 1 do asm lea eax, SameGlobalVariable lock inc [eax] //   end; WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); if SameGlobalVariable <> DoubleLimit then Writeln('Step two failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable); Readln; end.
      
      







この例の本質は、特定のグローバル変数SameGlobalVariable(タスクの元のステートメントからの行リンクのカウンターとして機能する)であり、スレッドを使用して通常モードとアトミックモードで値が変更されます。



ここでは、2つの動作モードの違いを明確に見ることができます。

コンソールには、次のようなものが表示されます。



 Step one failed. Expected: 2000000 but current: 1018924
      
      





第2の実施形態のエラーは表示されません。



ちなみに、最初のオプションはかなり良いランダマイザーとして使用できます(以前の記事で説明しました)。



要約すると:



特にDelphiおよびVCLシステムモジュールのソースコードを分析すると、実際にどのように動作するかについての仮定よりもはるかに多くの情報が得られることがありますが、これは事実ですが...







いいえ、これは事実ではありません、それは事実以上です-本当にそうだった方法



All Articles