介紹 在學習了sylar的C++高性能分散式伺服器框架後,想把自己在學習過程中的感想記錄下來。當然主要原因還是sylar的B站視頻過於難以理解了,也是想加強一下自己對這個框架的理解。很多內容也是借鑒了其他大佬的博文,比如找人找不到北,zhongluqiang 日誌模塊概述 日誌模塊的目的: 用於格式 ...
Safe locks for multi-thread applications(多線程應用程式的安全鎖)
由AB4327-GANDI,2016年1月9日。永久鏈接
一旦你的應用程式是多線程的,就應該保護併發數據訪問。我們已經寫過關於調試多線程應用程式可能很困難的文章。
否則,可能會出現“競態條件”問題:例如,如果兩個線程同時修改一個變數(例如減少計數器),值可能會變得不一致且不安全。邏輯錯誤的另一個癥狀是“死鎖”,當兩個線程錯誤地使用鎖時,會導致整個應用程式似乎被阻塞且無響應,從而相互阻塞。
在預期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文檔上繼續閱讀,其中可能包含有關此主題的更新和附加信息。