Safe locks for multi-thread applications(多執行緒應用程式的安全鎖)

海利鸟發表於2024-05-27

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文件上繼續閱讀,其中可能包含有關此主題的更新和附加資訊。

相關文章