在多執行緒環境下,確保資料同步和避免競爭條件至關重要。本文將比較兩種不同的同步方法:Windows API WaitForMultipleObjects 和跨平台的條件變數。前者適用於 Windows 平台,允許多個執行緒等待多個核心物件的訊號;後者則具有更廣泛的平台支援性,透過條件變數的等待和通知機制實作同步。兩種方法各有優劣,選擇哪種取決於專案的特定需求和目標平台。Delphi 提供了 TLockConditionVariable 作為條件變數的實作,簡化了跨平台多執行緒程式設計的複雜度。
與多個工作執行緒同步
在前面的章節中,我們實作了一個非常簡單的同步機制,用於讓執行緒接收新訊息的通知——等待事件。這種方法簡單有效,但靈活性不足。
當我們編寫複雜的系統時,一個執行緒會啟動多個工作執行緒,這種簡單的方法通常會失敗。我們需要更好的解決方案,能夠等待多個事件並在任意事件觸發時執行某些操作。Windows 提供了優秀的工具——WaitForMultipleObjects API 函式(有多種變體)——但這將我們鎖定在單一平台上。如果我們需要在其他作業系統上執行程式,該怎麼辦?
一個好的答案是條件變數。然而,它們在 Delphi RTL 中的實作有些奇怪(稍後會詳細說明),對於大多數 Windows Delphi 程式設計師來說仍然是一個完全的謎團。為了幫助任何想要深入研究的人,我準備了兩個簡單的多執行緒問題的解決方案:一個專門針對 Windows,另一個可以在任何支援的作業系統上使用。首先,讓我們來看看將它們聯絡在一起的測試程式。
MultipleWorkers 演示程式
MultipleWorkers 演示程式帶有兩個按鈕——WaitForMultiple 按鈕啟動特定於 Windows 的工作執行緒,而 CondVar 按鈕啟動與平台無關的解決方案。下面的程式碼片段顯示了 Windows 版本,它停用按鈕,建立測試物件,並啟動背景執行緒。測試物件建構函式接受兩個匿名函式,這些函式在工作執行緒上下文中被呼叫(因此不能修改 UI)。第一個函式(AsyncLogger)用於在螢幕上記錄資料,而第二個(內聯匿名函式)在測試停止執行時被呼叫,並簡單地重新啟用 UI 按鈕:
procedure TfrmMultipleWorkers.btnWaitForMultipleClick(Sender: TObject);
var
test: TWaitForMultipleTest;
begin
btnWaitForMultiple.Enabled := false;
lblOutput.Text := 'Output: ';
test := TWaitForMultipleTest.Create(10, AsyncLogger,
procedure
begin
TThread.Queue(nil,
procedure
begin
btnWaitForMultiple.Enabled := true;
end);
end);
test.Run;
end;
procedure TfrmMultipleWorkers.AsyncLogger(msg: string);
begin
TThread.Queue(nil,
procedure
begin
lblOutput.Text := lblOutput.Text + msg;
end);
end;
第二個按鈕的事件處理程式是相同的,除了它建立了一個 TCondVarTest 測試物件。程式碼使用一些 {$IFDEF WINDOWS} 編譯器指令,以便僅在 Windows 平台上啟用特定於 Windows 的程式碼。因此,您將能夠在 Windows 上比較兩種方法,或者在其他平台上比較與平台無關的程式碼。
兩種解決方案的共同演算法
- 測試物件建立背景執行緒。
- 背景執行緒建立兩個工作執行緒,並在接下來的 10 秒內等待來自兩個執行緒的訊號。之後,它停止工作執行緒並銷毀測試物件。
- 第一個執行緒每 700 毫秒喚醒一次,增加計數器,並通知背景執行緒。
- 第二個執行緒做同樣的事情,但每 1,200 毫秒。
使用 WaitForMultipleObjects
Run 方法(從主執行緒呼叫)只是建立一個匿名執行緒,告訴它執行 TestProc 方法,並啟動該執行緒。該執行緒組態為在 TestProc 離開時自動銷毀:
procedure TWaitForMultipleTest.Run;
var
thread: TThread;
begin
thread := TThread.CreateAnonymousThread(TestProc);
thread.FreeOnTerminate := true;
thread.Start;
end;
初始化工作執行緒
InitWorkers 方法建立了背景執行緒和工作執行緒之間同步所需的一切:
- 兩個鎖定物件
FLocks,用於在執行緒之間同步資料存取,每個工作執行緒一個。 - 兩個計數器
FCounters,每個工作執行緒一個。 - 兩個旗標
FStopped,由工作執行緒用於發出其執行/停止狀態的訊號。 - 兩個事件
FEvents,由工作執行緒用於喚醒背景執行緒。 FTotal,觸發事件的總數。FStop,一個用於向工作執行緒發出停止訊號的旗標。- 最後,兩個匿名工作執行緒——一個執行方法
Worker1和Worker2。
procedure TWaitForMultipleTest.InitWorkers;
begin
FLocks[1] := TCriticalSection.Create;
FLocks[2] := TCriticalSection.Create;
FCounters[1] := 0; FCounters[2] := 0;
FStopped[1] := false; FStopped[2] := false;
FEvents[1] := TEvent.Create(nil, false, false, '');
FEvents[2] := TEvent.Create(nil, false, false, '');
FTotal := 0;
FStop := false;
TThread.CreateAnonymousThread(Worker1).Start;
TThread.CreateAnonymousThread(Worker2).Start;
end;
工作執行緒方法
兩個工作執行緒執行相同的工作方法 RunWorker,只是引數不同:
procedure TWaitForMultipleTest.Worker1;
begin
RunWorker(1, 700);
end;
procedure TWaitForMultipleTest.Worker2;
begin
RunWorker(2, 1200);
end;
procedure TWaitForMultipleTest.RunWorker(idx, delay_ms: Integer);
begin
TThread.NameThreadForDebugging('Worker ' + IntToStr(idx));
while not FStop do begin
Sleep(delay_ms);
FLocks[idx].Acquire;
FCounters[idx] := FCounters[idx] + 1;
FLocks[idx].Release;
FEvents[idx].SetEvent;
end;
FStopped[idx] := true;
end;
背景執行緒方法
背景執行緒是最複雜的。它透過呼叫 InitWorkers 輔助方法初始化測試,並將兩個事件的控制程式碼複製到內部陣列中。然後,它執行一個迴圈,持續指定時間(10 秒)。
procedure TWaitForMultipleTest.TestProc;
var
awaited: cardinal;
handles: array [1..2] of THandle;
sw: TStopwatch;
begin
TThread.NameThreadForDebugging('Test runner');
InitWorkers;
handles[1] := FEvents[1].Handle;
handles[2] := FEvents[2].Handle;
sw := TStopwatch.StartNew;
while sw.Elapsed.Seconds < FDuration do begin
// 等待多個事件
awaited := WaitForMultipleObjects(2, @handles[1], false, 500);
// ...
詳細說明:
InitWorkers方法初始化了所有必要的同步物件和工作執行緒。- 工作執行緒定期更新計數器並設定事件以通知背景執行緒。
- 背景執行緒使用
WaitForMultipleObjects等待來自工作執行緒的事件,並根據收到的事件更新 UI。
圖表翻譯:
此圖示描述了背景執行緒與兩個工作執行緒之間的同步過程。背景執行緒等待來自任一工作執行緒的訊號,並據此更新 UI。
此圖示
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333
title 此圖示
rectangle "等待訊號" as node1
rectangle "更新計數器 & 設定事件" as node2
node1 --> node2
@enduml
圖表翻譯:
此圖表呈現了背景執行緒與兩個工作執行緒之間的互動過程。背景執行緒等待任一工作執行緒發出的訊號,而工作執行緒則定期更新計數器並設定事件以通知背景執行緒。
本章重點整理:
- 使用
WaitForMultipleObjects在 Windows 平台上實作多事件等待。 - 工作執行緒與背景執行緒之間的同步機制。
- 跨平台條件變數的使用方法。
未來,我們可以進一步探討更多關於多執行緒程式設計的最佳實踐,以及如何在不同平台上實作高效的執行緒同步和管理。透過不斷學習和實踐,我們能夠更好地掌握這些技術,從而開發出更高效、更穩定的多執行緒應用程式。
使用平行工具進行同步處理
在多執行緒程式設計中,同步處理是一項重要的技術。Delphi 提供了多種同步工具,例如 TConditionVariableCS 和 TConditionVariableMutex,但它們在不同平台上的實作方式不同。本文將介紹如何使用條件變數(Condition Variables)來實作多執行緒的同步處理。
條件變數的基本概念
條件變數是一種同步機制,允許執行緒等待特定的條件。它們在所有作業系統上都受到支援,但 API 可能有所不同。Delphi 的 System.SyncObjs 單元提供了 TConditionVariableCS 和 TConditionVariableMutex 類別,但它們的實作方式在不同平台上有所不同。
自訂的條件變數實作
為了避免使用 Delphi 內建的條件變數類別的複雜性,本文採用了一個輕量級的自訂實作,名為 TLightweightCondVar。這個實作使用原生作業系統的條件變數支援,可以在所有平台上運作。
TLockConditionVariable 記錄
TLockConditionVariable 是一個記錄型別,結合了條件變數和鎖定機制(在 Windows 上使用 TLightweightMREW,在其他系統上使用 pthread_mutex_t)。它提供了同步功能(Acquire 和 Release)、訊號功能(Signal 和 Broadcast)和等待功能(Wait 和 TryWait)。
type
TLockConditionVariable = record
private
FCondVar: TLightweightCondVar;
{$IFDEF MSWINDOWS}
FSRW: TLightweightMREW;
{$ENDIF MSWINDOWS}
{$IFDEF POSIX}
FMutex: pthread_mutex_t;
{$ENDIF POSIX}
public
{$IFDEF POSIX}
class operator Initialize(out Dest: TLockConditionVariable);
class operator Finalize(var Dest: TLockConditionVariable);
{$ENDIF POSIX}
procedure Acquire; inline;
procedure Release; inline;
procedure Broadcast; inline;
procedure Signal; inline;
procedure Wait; inline;
function TryWait(Timeout: Cardinal): boolean; inline;
end;
使用 TLockConditionVariable
TLockConditionVariable 可以用於多執行緒的同步處理。以下是一個範例,展示瞭如何使用 TLockConditionVariable 來實作多執行緒的同步處理。
初始化工作執行緒
在初始化工作執行緒時,我們需要建立一個 TLockConditionVariable 物件,並初始化相關的變數。
procedure TCondVarTest.InitWorkers;
begin
FCounters[1] := 0; FCounters[2] := 0;
// ...
end;
工作執行緒的實作
工作執行緒的實作與之前的範例類別似,但在等待條件變數時使用了 TLockConditionVariable。
procedure TCondVarTest.RunWorker(idx: Integer);
begin
// ...
FLockCondVar.Signal;
// ...
end;
主執行緒的實作
主執行緒可以使用 TLockConditionVariable 的 Wait 或 TryWait 方法來等待工作執行緒的完成。
procedure TCondVarTest.WaitForWorkers;
begin
FLockCondVar.Wait;
// ...
end;
程式碼解密:
上述程式碼展示瞭如何使用 TLockConditionVariable 來實作多執行緒的同步處理。其中,TLockConditionVariable 結合了條件變數和鎖定機制,提供了一種簡潔而有效的方式來實作多執行緒的同步處理。
圖表翻譯:
下圖展示了多執行緒同步處理的流程:
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333
title 圖表翻譯:
rectangle "等待" as node1
rectangle "訊號" as node2
rectangle "喚醒" as node3
node1 --> node2
node2 --> node3
@enduml
圖表翻譯: 此圖示展示了多執行緒同步處理的流程。主執行緒等待條件變數,而工作執行緒傳送訊號以喚醒主執行緒。
平行工具的工作原理
在多執行緒程式設計中,同步處理是一個非常重要的議題。本章節將探討使用條件變數(Condition Variables)來實作多執行緒同步的方法,並與使用 WaitForMultipleObjects 的 Windows 專屬實作進行比較。
使用條件變數的實作
首先,我們來看看使用條件變數的實作方式。條件變數是一種同步機制,允許執行緒等待某個條件的發生。在 Delphi 中,我們可以使用 TLockConditionVariable 來實作條件變數。
FChanged[1] := false;
FChanged[2] := false;
FStopped[1] := false;
FStopped[2] := false;
FTotal := 0;
FStop := false;
TThread.CreateAnonymousThread(Worker1).Start;
TThread.CreateAnonymousThread(Worker2).Start;
工作執行緒的實作
procedure TCondVarTest.RunWorker(idx, delay_ms: Integer);
begin
TThread.NameThreadForDebugging('Worker ' + IntToStr(idx));
while not FStop do
begin
Sleep(delay_ms);
FCondVar.Acquire;
FCounters[idx] := FCounters[idx] + 1;
FChanged[idx] := true;
FCondVar.Release;
FCondVar.Signal;
end;
FStopped[idx] := true;
end;
內容解密:
- 在
RunWorker程式中,我們首先為執行緒命名,以便於偵錯。 - 執行緒進入迴圈後,會根據
FStop的值決定是否繼續執行。 - 使用
FCondVar.Acquire來取得鎖定,以確保對分享資源的存取是安全的。 - 對
FCounters[idx]進行遞增操作,並將FChanged[idx]設定為true,表示該執行緒已經完成了工作。 - 使用
FCondVar.Release來釋放鎖定,並使用FCondVar.Signal來通知其他執行緒。
主執行緒的實作
主執行緒負責等待工作執行緒的完成,並處理結果。
procedure TCondVarTest.TestProc;
var
sw: TStopwatch;
begin
TThread.NameThreadForDebugging('Test runner');
InitWorkers;
sw := TStopwatch.StartNew;
FCondVar.Acquire;
while sw.Elapsed.Seconds < FDuration do
begin
if not FCondVar.TryWait(500) then
FLogger('.')
else
begin
if FChanged[1] then
begin
GrabCounter(1);
FChanged[1] := false;
FLogger('x');
end;
if FChanged[2] then
begin
GrabCounter(2);
FChanged[2] := false;
FLogger('o');
end;
end;
end;
FCondVar.Release;
StopWorkers;
FLogger(' ' + IntToStr(FTotal) + '/' + IntToStr(FCounters[1]) + '/' + IntToStr(FCounters[2]));
FOnStop();
end;
內容解密:
- 主執行緒首先初始化工作執行緒,並啟動計時器。
- 使用
FCondVar.Acquire來取得鎖定,並進入迴圈等待工作執行緒的完成。 - 在迴圈中,使用
FCondVar.TryWait(500)來等待條件變數的訊號,如果超時則輸出.。 - 當收到訊號後,檢查
FChanged陣列,以確定哪個執行緒完成了工作,並呼叫GrabCounter程式來處理結果。 - 處理完成後,釋放鎖定並停止工作執行緒。
與 Windows 專屬實作的比較
兩種實作方式的主要差異在於同步機制的不同。Windows 專屬實作使用 WaitForMultipleObjects,而條件變數實作使用 TLockConditionVariable。
Windows 專屬實作的特點:
- 使用分享鎖定和事件來進行同步。
- 工作執行緒遵循
acquire/modify/release/signal的模式。 - 主執行緒遵循
wait/acquire/process/release的模式。
條件變數實作的特點:
- 使用分享的
TLockConditionVariable,不需要建立或銷毀。 - 工作執行緒遵循
acquire/modify/release/signal的模式。 - 主執行緒遵循
acquire/wait/process/release的模式。