在 Delphi 的多執行緒應用程式中,確保資料一致性和避免競爭條件至關重要,同步機制正是解決此問題的關鍵。然而,不當使用同步機制可能導致死鎖,使程式陷入停滯。本文將探討死鎖的成因、偵測方法與解決方案,並深入解析 Delphi 提供的各種同步機制,包括 Mutex、Semaphore、TMonitor、TSpinLock、TLightweightMREW 和原子操作等,同時也將探討物件生命週期管理在多執行緒程式設計中的重要性。藉由程式碼範例和效能比較,協助開發者更有效地運用這些技術,編寫出更穩定、高效的多執行緒應用程式。

同步機制與死鎖問題

在多執行緒程式設計中,同步機制是確保資料安全與避免競爭條件的關鍵。然而,不當的同步機制使用可能導致死鎖(deadlock)的發生。本章節將探討死鎖的成因、偵測方法及解決方案,並介紹其他同步機制,如 Mutex 與 Semaphore。

死鎖的成因與偵測

死鎖發生於兩個或多個執行緒互相等待對方釋放資源,導致所有相關執行緒都無法繼續執行的情況。在前述範例中,兩個執行緒 TaskProc1TaskProc2 分別嘗試取得兩個不同的鎖(LockCounterLockOther),但取得順序相反,從而導致死鎖。

procedure TaskProc1(const task: ITask; const shared: TSharedData);
begin
  while frmDeadlock.FTask1.Status <> TTaskStatus.Canceled do begin
    shared.LockCounter.Acquire;
    shared.LockOther.Acquire;
    shared.Counter := shared.Counter + 1;
    shared.LockOther.Release;
    shared.LockCounter.Release;
  end;
end;

procedure TaskProc2(const task: ITask; const shared: TSharedData);
begin
  while frmDeadlock.FTask1.Status <> TTaskStatus.Canceled do begin
    shared.LockOther.Acquire;
    shared.LockCounter.Acquire;
    shared.Counter := shared.Counter + 1;
    shared.LockCounter.Release;
    shared.LockOther.Release;
  end;
end;

內容解密:

  • TaskProc1TaskProc2 皆進入無限迴圈,直到任務被取消。
  • 每個程式先取得一個鎖,然後嘗試取得另一個鎖,若無法取得則阻塞。
  • 當兩個執行緒分別持有一個鎖並等待另一個鎖時,死鎖發生。

偵測死鎖的方法包括使用偵錯工具暫停程式執行,檢查各執行緒的呼叫堆積疊(Call Stack),找出阻塞在 Acquire 呼叫的執行緒。

解決死鎖的方法

  1. 良好的程式設計:盡量縮短持有鎖的時間,避免在持有鎖的情況下呼叫其他可能取得鎖的程式碼。
  2. 統一鎖的取得順序:確保所有執行緒以相同的順序取得多個鎖。
  3. 使用 TryEnter:使用 TryEnter 方法嘗試取得鎖,若失敗則釋放已持有的鎖,避免死鎖。
procedure TryTaskProc1(const task: ITask; const shared: TSharedData);
begin
  while frmDeadlock.FTask1.Status <> TTaskStatus.Canceled do begin
    shared.LockCounter.Acquire;
    if shared.LockOther.TryEnter then begin
      shared.Counter := shared.Counter + 1;
      shared.LockOther.Leave;
    end;
    shared.LockCounter.Release;
  end;
end;

procedure TryTaskProc2(const task: ITask; const shared: TSharedData);
begin
  while frmDeadlock.FTask1.Status <> TTaskStatus.Canceled do begin
    shared.LockOther.Acquire;
    if shared.LockCounter.TryEnter then begin
      shared.Counter := shared.Counter + 1;
      shared.LockCounter.Leave;
    end;
    shared.LockOther.Release;
  end;
end;

內容解密:

  • 使用 TryEnter 嘗試取得第二個鎖,若失敗則不進行計數器操作,直接釋放第一個鎖。
  • 這種方法避免了死鎖,但可能導致效能下降,因為執行緒不斷嘗試取得鎖。

其他同步機制

Mutex

Mutex(互斥量)與 Critical Section 類別似,但 Mutex 可以跨行程使用,透過名稱識別。Mutex 的使用方法與 Critical Section 相似,但效能較低。

var
  Mutex: TMutex;
begin
  Mutex := TMutex.Create(nil, False, 'MyMutex');
  try
    Mutex.Acquire;
    try
      // 臨界區程式碼
    finally
      Mutex.Release;
    end;
  finally
    Mutex.Free;
  end;
end;

Semaphore

Semaphore(訊號量)用於控制對有限資源的存取。當資源數量大於零時,執行緒可以取得 Semaphore 並繼續執行;當資源數量為零時,執行緒將被阻塞。

var
  Semaphore: TSemaphore;
begin
  Semaphore := TSemaphore.Create(nil, 3, 3, 'MySemaphore');
  try
    if Semaphore.Acquire then begin
      try
        // 使用資源
      finally
        Semaphore.Release;
      end;
    end;
  finally
    Semaphore.Free;
  end;
end;

進入平行世界:同步機制解析

在多執行緒程式設計中,同步機制是確保資料安全與執行緒間協作的關鍵。本章將探討Delphi中各種同步機制的實作與應用。

旗號(Semaphore)

旗號是一種進階同步機制,允許一定數量的執行緒同時存取分享資源。它內部維護一個計數器,當計數器為零時,執行緒將被阻塞。旗號的Release方法會增加計數器,允許其他執行緒進入臨界區。

雖然旗號功能強大,但由於其效能與互斥鎖相近,本文不再探討。有興趣的讀者可以參考《The Little Book of Semaphores》一書。

TMonitor:物件層級的鎖定機制

TMonitor是Delphi 2009引入的同步機制,它擴充套件了TObject類別,提供了物件層級的鎖定功能。雖然最初的實作存在一些問題,但近年來的更新已經修復了這些缺陷,使其成為一個可靠的同步工具。

TMonitor 使用範例

procedure TfrmIncDec.MonitorLockedDecValue;
var
  value: integer;
  i: Integer;
begin
  for i := 1 to CNumRepeat do begin
    System.TMonitor.Enter(Self);
    try
      value := FValue;
      FValue := value + 1;
    finally
      System.TMonitor.Exit(Self);
    end;
  end;
end;

內容解密:

  1. System.TMonitor.Enter(Self):對當前表單物件進行加鎖,確保同一時間只有一個執行緒可以存取該物件。
  2. try...finally結構確保在操作完成後,無論是否發生異常,都會執行System.TMonitor.Exit(Self)來釋放鎖。
  3. 這種方式比使用TCriticalSection更快,因為它採用了自旋鎖(spinlock)的實作。

自旋鎖(Spinlock)

自旋鎖是一種高效的同步機制,它假設臨界區的程式碼非常短暫。當一個執行緒嘗試取得已被佔用的自旋鎖時,它不會立即進入睡眠狀態,而是會在一個緊密的迴圈中不斷檢查鎖是否可用。

TSpinLock 使用範例

procedure TfrmIncDec.SpinlockDecValue;
var
  i: integer;
  value: integer;
begin
  for i := 1 to CNumRepeat do begin
    FSpinlock.Enter;
    try
      value := FValue;
      FValue := value - 1;
    finally
      FSpinlock.Exit;
    end;
  end;
end;

內容解密:

  1. FSpinlock.Enter:嘗試取得自旋鎖,如果鎖已被佔用,執行緒將進入忙等待狀態。
  2. try...finally結構確保在操作完成後釋放鎖。
  3. TSpinLock不是可重入的,如果同一個執行緒多次呼叫Enter,可能會導致死鎖或異常。

讀寫鎖(Readers-Writer Lock)

在許多應用場景中,資料被讀取的頻率遠高於被修改的頻率。讀寫鎖允許多個讀取執行緒同時存取分享資源,但寫入執行緒會獨佔資源並阻塞所有讀取操作。

TLightweightMREW 使用範例

var
  MREW: TLightweightMREW;

procedure ReadData;
begin
  MREW.BeginRead;
  try
    // 讀取資料
  finally
    MREW.EndRead;
  end;
end;

procedure WriteData;
begin
  MREW.BeginWrite;
  try
    // 修改資料
  finally
    MREW.EndWrite;
  end;
end;

圖表翻譯:

此圖示展示了不同同步機制的效能比較,包括TMonitor、TCriticalSection、TSpinLock等。可以看出,TMonitor和TSpinLock在大多數情況下表現更優。

平行世界入門:同步機制詳解

在多執行緒程式設計中,同步機制是確保資料安全和避免競爭條件的關鍵。Delphi 提供多種同步工具,包括 TLightweightMREWTThreadListTThreadedQueue<T> 等。本篇文章將探討這些同步機制的原理和應用。

使用 TLightweightMREW 進行讀寫鎖定

TLightweightMREW 是一種讀寫鎖定機制,允許多個執行緒同時讀取分享資源,但寫入操作需要獨佔存取。使用 TLightweightMREW 時,需要在讀取前呼叫 BeginRead,寫入前呼叫 BeginWrite,並在操作完成後分別呼叫 EndReadEndWrite

程式碼範例

var
  LLock: TLightweightMREW;
begin
  LLock.BeginRead;
  try
    // 讀取分享資源
  finally
    LLock.EndRead;
  end;

  LLock.BeginWrite;
  try
    // 寫入分享資源
  finally
    LLock.EndWrite;
  end;
end;

內容解密:

  1. BeginReadBeginWrite 用於取得讀取或寫入鎖定。
  2. EndReadEndWrite 用於釋放鎖定。
  3. 錯誤使用 EndWrite 結束 BeginReadEndRead 結束 BeginWrite 將導致程式當機或死鎖。

TLightweightMREW 的實作細節

與大多數 Delphi 同步物件不同,TLightweightMREW 是以記錄(record)形式實作的。這意味著無需建立或銷毀 TLightweightMREW 例項,只需宣告一個該型別的欄位即可。

注意事項

  • 讀取鎖定是可重入的,但寫入鎖定不可重入。若在已取得寫入鎖定的執行緒中再次呼叫 BeginWrite,程式將死鎖(Windows)或引發異常(其他平台)。

資源鎖定的最佳實踐

在實際程式設計中,建議使用 try..finally 區塊來確保資源正確釋放。例如:

FSpinlock.Enter;
try
  // 操作分享資源
finally
  FSpinlock.Exit;
end;

內容解密:

  1. 使用 try..finally 可確保即使程式發生異常,資源也能被正確釋放。
  2. 這種寫法使資源的取得和釋放成對出現,提高了程式碼的可讀性。

Delphi 內建的執行緒安全資料結構

Delphi 提供多種內建的執行緒安全資料結構,如 TThreadListTThreadList<T>TThreadedQueue<T>。這些資料結構內部已實作必要的同步機制,簡化了多執行緒程式設計。

TThreadList 使用範例

var
  list: TList<Integer>;
begin
  list := FThreadList.LockList;
  try
    // 操作 list
  finally
    FThreadList.UnlockList;
  end;
end;

內容解密:

  1. LockList 傳回內部的 TList 物件,並取得必要的鎖定。
  2. 操作完成後,必須呼叫 UnlockList 以釋放鎖定。
  3. 在呼叫 UnlockList 後繼續使用傳回的 list 將導致未定義行為。

無鎖定程式設計(Lock-Free Programming)

對於小型分享資料,可以使用無鎖定操作,如 InterlockedIncrementInterlockedExchange,來避免鎖定開銷。這些操作利用 CPU 的原子指令實作高效的同步。

平行世界入門:同步技術詳解

在多執行緒程式設計中,同步技術是確保資料正確性與避免競爭條件的關鍵。Delphi 提供多種同步機制,其中包括使用 TInterlocked 類別進行原子操作。本文將探討 TInterlocked 的使用方法及其背後的原理。

使用 TInterlocked 進行原子操作

TInterlocked 類別位於 System.SyncObjs 單元中,提供了一系列原子操作函式。這些函式大多是對 System 單元中與 CPU 相依的 Atomic* 函式的簡單封裝。例如,TInterlocked.Add 函式內部呼叫了 AtomicIncrement

class function TInterlocked.Add(var Target: Integer; Increment: Integer): Integer;
begin
  Result := AtomicIncrement(Target, Increment);
end;

由於 TInterlocked 的函式都被宣告為行內函式,因此直接呼叫 TInterlocked.Add 與呼叫 AtomicIncrement 的效能是一樣的。

原子操作函式家族

原子操作函式可以分為兩大類別:一類別用於修改共用值,另一類別用於交換兩個值。

  1. 修改共用值:這類別函式包括 IncrementDecrementAdd 等。它們對共用值進行原子性的修改,並傳回新值。

    procedure TfrmIncDec.InterlockedIncValue;
    var
      i: integer;
    begin
      for i := 1 to CNumRepeat do
        TInterlocked.Increment(FValue);
    end;
    
    procedure TfrmIncDec.InterlockedDecValue;
    var
      i: integer;
    begin
      for i := 1 to CNumRepeat do
        TInterlocked.Decrement(FValue);
    end;
    

    內容解密:

    • TInterlocked.IncrementTInterlocked.Decrement 分別對 FValue 進行原子性的遞增和遞減操作。
    • 這兩個函式保證了在多執行緒環境下對 FValue 的修改是執行緒安全的。
  2. 交換值:這類別函式主要包括 ExchangeCompareExchange。它們用於原子性地交換或根據條件更新共用值。

    • Exchange 函式簡單地將共用資料替換為新值,並傳回原始值。

      class function TInterlocked.Exchange(var Target: Integer; Value: Integer): Integer;
      begin
        Result := Target;
        Target := Value;
      end;
      

      內容解密:

      • Exchange 實作了原子性的值交換,保證在多執行緒環境下的安全性。
      • 它傳回原始值並將共用資料設定為新值。
    • CompareExchange 函式比較共用資料與測試值,如果相等,則將共用資料設定為新值,並傳回共用資料的原始值。

      class function CompareExchange(var Target: Integer; Value: Integer; Comparand: Integer): Integer;
      begin
        Result := Target;
        if Target = Comparand then
          Target := Value;
      end;
      

      內容解密:

      • CompareExchange 用於根據條件原子性地更新共用資料。
      • 如果原始值與 Comparand 相等,則更新為 Value,否則保持不變。
      • 傳回原始值以便檢查是否發生了更新。

使用 TInterlocked.Read 進行原子讀取

在某些情況下,我們需要原子性地讀取共用資料。TInterlocked.Read 函式透過呼叫 CompareExchange 來實作這一功能:

class function TInterlocked.Read(var Target: Int64): Int64;
begin
  Result := CompareExchange(Target, 0, 0);
end;

內容解密:

  • TInterlocked.Read 利用 CompareExchange 的特性實作了對 Int64 型別資料的原子讀取。
  • 即使原始值為 0 或非 0,CompareExchange(Target, 0, 0) 都能正確傳回原始值,因為它不會改變原始值。

範例應用:使用 TInterlocked 重構 ReadWrite 示範

透過使用 TInterlocked.ReadTInterlocked.Exchange,我們可以重構 ReadWrite 示範以避免使用鎖定機制:

procedure TfrmReadWrite.InterlockedReader;
var
  i: integer;
  value: int64;
begin
  for i := 1 to CNumRepeat do begin
    value := TInterlocked.Read(FPValue^);
    FValueList.Add(value);
  end;
end;

procedure TfrmReadWrite.InterlockedWriter;
var
  i: integer;
begin
  for i := 1 to CNumRepeat do begin
    TInterlocked.Exchange(FPValue^, $7777777700000000);
    TInterlocked.Exchange(FPValue^, $0000000077777777);
  end;
end;

圖表翻譯:

此圖示展示了使用 TInterlocked 操作與鎖定機制之間的效能比較。可以看到,使用 TInterlocked 能夠提升約 20% 的效能。

物件生命週期管理

在多執行緒環境中,正確管理物件的生命週期至關重要。最簡單的方法是在存取物件時使用鎖定機制,但這可能導致效能問題。另一種方法是確保物件在背景任務啟動前建立,並在背景任務結束後銷毀。這種方法利用了參考計數機制在 ARC 平台上的執行緒安全性。

注意事項:

  • 在多執行緒環境中使用物件時,必須確保物件的方法是執行緒安全的。
  • Delphi 不再支援 ARC,但瞭解 ARC 的工作原理仍然對理解某些程式碼行為有所幫助。

總之,正確使用 TInterlocked 和理解物件生命週期管理是編寫高效、正確的多執行緒程式的關鍵。