在 Delphi 的多執行緒應用程式中,確保資料一致性和避免競爭條件至關重要,同步機制正是解決此問題的關鍵。然而,不當使用同步機制可能導致死鎖,使程式陷入停滯。本文將探討死鎖的成因、偵測方法與解決方案,並深入解析 Delphi 提供的各種同步機制,包括 Mutex、Semaphore、TMonitor、TSpinLock、TLightweightMREW 和原子操作等,同時也將探討物件生命週期管理在多執行緒程式設計中的重要性。藉由程式碼範例和效能比較,協助開發者更有效地運用這些技術,編寫出更穩定、高效的多執行緒應用程式。
同步機制與死鎖問題
在多執行緒程式設計中,同步機制是確保資料安全與避免競爭條件的關鍵。然而,不當的同步機制使用可能導致死鎖(deadlock)的發生。本章節將探討死鎖的成因、偵測方法及解決方案,並介紹其他同步機制,如 Mutex 與 Semaphore。
死鎖的成因與偵測
死鎖發生於兩個或多個執行緒互相等待對方釋放資源,導致所有相關執行緒都無法繼續執行的情況。在前述範例中,兩個執行緒 TaskProc1 和 TaskProc2 分別嘗試取得兩個不同的鎖(LockCounter 和 LockOther),但取得順序相反,從而導致死鎖。
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;
內容解密:
TaskProc1和TaskProc2皆進入無限迴圈,直到任務被取消。- 每個程式先取得一個鎖,然後嘗試取得另一個鎖,若無法取得則阻塞。
- 當兩個執行緒分別持有一個鎖並等待另一個鎖時,死鎖發生。
偵測死鎖的方法包括使用偵錯工具暫停程式執行,檢查各執行緒的呼叫堆積疊(Call Stack),找出阻塞在 Acquire 呼叫的執行緒。
解決死鎖的方法
- 良好的程式設計:盡量縮短持有鎖的時間,避免在持有鎖的情況下呼叫其他可能取得鎖的程式碼。
- 統一鎖的取得順序:確保所有執行緒以相同的順序取得多個鎖。
- 使用
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;
內容解密:
System.TMonitor.Enter(Self):對當前表單物件進行加鎖,確保同一時間只有一個執行緒可以存取該物件。try...finally結構確保在操作完成後,無論是否發生異常,都會執行System.TMonitor.Exit(Self)來釋放鎖。- 這種方式比使用
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;
內容解密:
FSpinlock.Enter:嘗試取得自旋鎖,如果鎖已被佔用,執行緒將進入忙等待狀態。try...finally結構確保在操作完成後釋放鎖。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 提供多種同步工具,包括 TLightweightMREW、TThreadList 和 TThreadedQueue<T> 等。本篇文章將探討這些同步機制的原理和應用。
使用 TLightweightMREW 進行讀寫鎖定
TLightweightMREW 是一種讀寫鎖定機制,允許多個執行緒同時讀取分享資源,但寫入操作需要獨佔存取。使用 TLightweightMREW 時,需要在讀取前呼叫 BeginRead,寫入前呼叫 BeginWrite,並在操作完成後分別呼叫 EndRead 和 EndWrite。
程式碼範例
var
LLock: TLightweightMREW;
begin
LLock.BeginRead;
try
// 讀取分享資源
finally
LLock.EndRead;
end;
LLock.BeginWrite;
try
// 寫入分享資源
finally
LLock.EndWrite;
end;
end;
內容解密:
BeginRead和BeginWrite用於取得讀取或寫入鎖定。EndRead和EndWrite用於釋放鎖定。- 錯誤使用
EndWrite結束BeginRead或EndRead結束BeginWrite將導致程式當機或死鎖。
TLightweightMREW 的實作細節
與大多數 Delphi 同步物件不同,TLightweightMREW 是以記錄(record)形式實作的。這意味著無需建立或銷毀 TLightweightMREW 例項,只需宣告一個該型別的欄位即可。
注意事項
- 讀取鎖定是可重入的,但寫入鎖定不可重入。若在已取得寫入鎖定的執行緒中再次呼叫
BeginWrite,程式將死鎖(Windows)或引發異常(其他平台)。
資源鎖定的最佳實踐
在實際程式設計中,建議使用 try..finally 區塊來確保資源正確釋放。例如:
FSpinlock.Enter;
try
// 操作分享資源
finally
FSpinlock.Exit;
end;
內容解密:
- 使用
try..finally可確保即使程式發生異常,資源也能被正確釋放。 - 這種寫法使資源的取得和釋放成對出現,提高了程式碼的可讀性。
Delphi 內建的執行緒安全資料結構
Delphi 提供多種內建的執行緒安全資料結構,如 TThreadList、TThreadList<T> 和 TThreadedQueue<T>。這些資料結構內部已實作必要的同步機制,簡化了多執行緒程式設計。
TThreadList 使用範例
var
list: TList<Integer>;
begin
list := FThreadList.LockList;
try
// 操作 list
finally
FThreadList.UnlockList;
end;
end;
內容解密:
LockList傳回內部的TList物件,並取得必要的鎖定。- 操作完成後,必須呼叫
UnlockList以釋放鎖定。 - 在呼叫
UnlockList後繼續使用傳回的 list 將導致未定義行為。
無鎖定程式設計(Lock-Free Programming)
對於小型分享資料,可以使用無鎖定操作,如 InterlockedIncrement 和 InterlockedExchange,來避免鎖定開銷。這些操作利用 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 的效能是一樣的。
原子操作函式家族
原子操作函式可以分為兩大類別:一類別用於修改共用值,另一類別用於交換兩個值。
修改共用值:這類別函式包括
Increment、Decrement和Add等。它們對共用值進行原子性的修改,並傳回新值。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.Increment和TInterlocked.Decrement分別對FValue進行原子性的遞增和遞減操作。- 這兩個函式保證了在多執行緒環境下對
FValue的修改是執行緒安全的。
交換值:這類別函式主要包括
Exchange和CompareExchange。它們用於原子性地交換或根據條件更新共用值。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.Read 和 TInterlocked.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 和理解物件生命週期管理是編寫高效、正確的多執行緒程式的關鍵。