Safe locks for multi-thread applications(多執行緒應用程式的安全鎖)
由AB4327-GANDI,2016年1月9日。永久連結
開源 › mORMot框架
- 部落格
- 臨界區
- Delphi
- 良好實踐
- mORMot
- 多執行緒
一旦你的應用程式是多執行緒的,就應該保護併發資料訪問。我們已經寫過關於除錯多執行緒應用程式可能很困難的文章。
否則,可能會出現“競態條件”問題:例如,如果兩個執行緒同時修改一個變數(例如減少計數器),值可能會變得不一致且不安全。邏輯錯誤的另一個症狀是“死鎖”,當兩個執行緒錯誤地使用鎖時,會導致整個應用程式似乎被阻塞且無響應,從而相互阻塞。
在預期24/7執行且無需維護的伺服器系統上,應避免此類問題。
在Delphi中,資源(可能是一個物件或任何變數)的保護通常透過臨界區來實現。
臨界區是一個物件,用於確保程式碼的一部分一次只能由一個執行緒執行。臨界區需要在使用之前建立/初始化,並在不再需要時釋放。然後,一些程式碼透過使用Enter/Leave方法進行保護,這將鎖定其執行:實際上,只有一個執行緒會擁有臨界區,所以只有一個執行緒能夠執行這段程式碼,其他執行緒將等待直到鎖被釋放。為了獲得最佳效能,受保護的區域應儘可能小——否則,使用執行緒的好處可能會失效,因為任何其他執行緒都會等待擁有臨界區的執行緒釋放鎖。
我們現在將看到Delphi的 TCriticalSection
可能存在的問題,以及我們的框架提出簡化臨界區在您的應用程式中的使用。
注:在Delphi中,TCriticalSection
是用於管理執行緒同步的一個類。當多個執行緒需要訪問共享資源時,可以使用 TCriticalSection
來確保每次只有一個執行緒可以訪問該資源,從而防止資料競爭和不一致。然而,TCriticalSection
的使用也可能帶來一些問題,比如死鎖或者效能瓶頸,因此需要謹慎使用。mORMot框架提供了一些工具和策略來簡化 TCriticalSection
的使用,並幫助開發者更安全、更有效地管理執行緒同步。
修復 TRTLCriticalSection
在實踐中,您可能會使用一個 TCriticalSection
類,或者更低階別的 TRTLCriticalSection
記錄,後者可能是更好的選擇,因為它使用的記憶體更少,並且可以很容易地作為任何 class
定義的(受保護)欄位包含進去。
假設我們要保護對變數a和b的任何訪問。以下是如何使用臨界區方法來實現:
var CS: TRTLCriticalSection;
a, b: integer;
// 線上程開始前設定
InitializeCriticalSection(CS);
// 在每個TThread.Execute中:
EnterCriticalSection(CS);
try // 透過try...finally塊保護鎖
// 從現在開始,您可以安全地更改變數
inc(a);
inc(b);
finally
// 安全塊結束
LeaveCriticalSection(CS);
end;
// 當執行緒停止時
DeleteCriticalSection(CS);
在最新版本的Delphi中,您可以使用 TMonitor
類,它允許任何Delphi TObject
擁有鎖。
在XE5之前,存在一些效能問題,即使到現在,這個受Java啟發的特性可能也不是最佳方法,因為它與單個物件繫結,並且與較舊版本的Delphi(或FPC)不相容。
幾年前,Eric Grange報告說——參見這篇部落格文章——TRTLCriticalSection
(連同 TMonitor
)存在嚴重的設計缺陷,進入/離開不同的臨界區可能會使您的執行緒序列化,甚至整個效能可能比執行緒被序列化時更差。這是因為它是一個小的、動態分配的物件,所以幾個 TRTLCriticalSection
的記憶體可能最終會落在同一個CPU快取行中,當發生這種情況時,執行執行緒的核心之間會發生大量的快取衝突。
Eric提出的修復方法非常簡單:
type
TFixedCriticalSection = class(TCriticalSection)
private
FDummy: array [0..95] of Byte;
end;
從T*Locked繼承
在定義您自己的類時,您可以繼承一些提供 TSynLocker
例項的類,如在 SynCommons.pas
中定義的:
TSynPersistentLocked = class(TSynPersistent)
...
property Safe: TSynLocker read fSafe;
end;
TInterfacedObjectLocked = class(TInterfacedObjectWithCustomCreate)
...
property Safe: TSynLocker read fSafe;
end;
TObjectListLocked = class(TObjectList)
...
property Safe: TSynLocker read fSafe;
end;
TRawUTF8ListHashedLocked = class(TRawUTF8ListHashed)
...
property Safe: TSynLocker read fSafe;
end;
所有這些類都將在其 constructor/destructor
中初始化和終結它們所擁有的 Safe
例項。
因此,我們可以這樣編寫我們的類:
type
TMyClass = class(TSynPersistentLocked)
protected
fField: integer;
public
procedure UseLockUnlock;
procedure UseProtectMethod;
end;
{ TMyClass }
procedure TMyClass.UseLockUnlock;
begin
fSafe.Lock;
try
// 現在我們可以安全地從多個執行緒訪問任何受保護的欄位
inc(fField);
finally
fSafe.UnLock;
end;
end;
procedure TMyClass.UseProtectMethod;
begin
fSafe.ProtectMethod; // 呼叫fSafe.Lock並返回IUnknown本地例項
// 現在我們可以安全地從多個執行緒訪問任何受保護的欄位
inc(fField);
// 當IUnknown被釋放時,將呼叫fSafe.UnLock
end;
如您所見,Safe: TSynLocker
例項將在 TSynPersistentLocked
父級定義並處理。
注入IAutoLocker例項
如果您的類繼承自 TInjectableObject
,您甚至可以定義以下內容:
type
TMyClass = class(TInjectableObject)
private
fLock: IAutoLocker;
fField: integer;
public
function FieldValue: integer;
published
property Lock: IAutoLocker read fLock write fLock;
end;
{ TMyClass }
function TMyClass.FieldValue: integer;
begin
Lock.ProtectMethod;
result := fField;
inc(fField);
end;
var c: TMyClass;
begin
c := TMyClass.CreateInjected([],[],[]);
Assert(c.FieldValue=0);
Assert(c.FieldValue=1);
c.Free;
end;
在這裡,我們使用了依賴解析——請參閱[依賴注入和介面解析](http://synopse.info/files/html/Synopse mORMot Framework SAD 1.18.html#TITL_161)——讓 TMyClass.CreateInjected
建構函式掃描其 published
屬性,從而搜尋 IAutoLocker
的提供者。由於 IAutoLocker
已全域性註冊為透過 TAutoLocker
解析,因此我們的類將使用新例項初始化其 fLock
欄位。現在,我們可以像往常一樣使用 Lock.ProtectMethod
來訪問關聯的 TSynLocker
臨界區。
當然,這可能會比手動處理 TSynLocker
更復雜,但是如果您正在編寫一個基於介面的服務,您的類可以從 TInjectableObject
繼承以進行自身的依賴解析,因此這個技巧可能非常方便。
TSynLocker中的安全鎖定儲存
當我們解決了潛在的CPU快取行問題時,您還記得我們在 TSynLocker
定義中新增了一個填充二進位制緩衝區嗎?由於我們不想浪費資源,TSynLocker
提供了對其內部資料的輕鬆訪問,並允許直接處理這些值。由於它儲存為7個 variant
值插槽,因此您可以儲存任何型別的資料,包括複雜的 TDocVariant
文件或陣列。
我們的類可以使用此功能,並將其整數字段值儲存在內部插槽0中:
type
TMyClass = class(TSynPersistentLocked)
public
procedure UseInternalIncrement;
function FieldValue: integer;
end;
{ TMyClass }
function TMyClass.FieldValue: integer;
begin // 值的讀取也將受到互斥鎖的保護
result := fSafe.LockedInt64[0];
end;
procedure TMyClass.UseInternalIncrement;
begin // 這個專用的方法將確保原子增加
fSafe.LockedInt64Increment(0,1);
end;
請注意,我們使用了 TSynLocker.LockedInt64Increment()
方法,因為以下方式是不安全的:
procedure TMyClass.UseInternalIncrement;
begin
fSafe.LockedInt64[0] := fSafe.LockedInt64[0]+1;
end;
在上面的程式碼中,獲取了兩個鎖(每個 LockedInt64
屬性呼叫一個),因此另一個執行緒可能會在兩者之間修改值,並且增量可能不如預期準確。
TSynLocker
提供了一些專用的屬性和方法來處理這種安全的儲存。這些期望一個 Index
值,範圍從 0..6
:
property Locked[Index: integer]: Variant read GetVariant write SetVariant;
property LockedInt64[Index: integer]: Int64 read GetInt64 write SetInt64;
property LockedPointer[Index: integer]: Pointer read GetPointer write SetPointer;
property LockedUTF8[Index: integer]: RawUTF8 read GetUTF8 write SetUTF8;
function LockedInt64Increment(Index: integer; const Increment: Int64): Int64;
function LockedExchange(Index: integer; const Value: variant): variant;
function LockedPointerExchange(Index: integer; Value: pointer): pointer;
如果有必要,您可以儲存一個 pointer
或對 TObject
例項的引用。
在我們的框架中,提供這樣一套執行緒安全的方法是有意義的,該框架提供了多執行緒伺服器能力——請參閱執行緒安全性。
請隨時在mORMot文件上繼續閱讀,其中可能包含有關此主題的更新和附加資訊。