返回文章列表

多執行緒同步技術探討

本文探討多執行緒程式設計中的同步處理技術,比較了 Windows 平台上的 WaitForMultipleObjects 與跨平台的條件變數方法,並深入剖析 Delphi 中使用 TLockConditionVariable 實作多執行緒同步的機制和原理,同時提供程式碼範例和圖表說明。

多執行緒 系統設計

在多執行緒環境下,確保資料同步和避免競爭條件至關重要。本文將比較兩種不同的同步方法: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 上比較兩種方法,或者在其他平台上比較與平台無關的程式碼。

兩種解決方案的共同演算法

  1. 測試物件建立背景執行緒。
  2. 背景執行緒建立兩個工作執行緒,並在接下來的 10 秒內等待來自兩個執行緒的訊號。之後,它停止工作執行緒並銷毀測試物件。
  3. 第一個執行緒每 700 毫秒喚醒一次,增加計數器,並通知背景執行緒。
  4. 第二個執行緒做同樣的事情,但每 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,一個用於向工作執行緒發出停止訊號的旗標。
  • 最後,兩個匿名工作執行緒——一個執行方法 Worker1Worker2
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 提供了多種同步工具,例如 TConditionVariableCSTConditionVariableMutex,但它們在不同平台上的實作方式不同。本文將介紹如何使用條件變數(Condition Variables)來實作多執行緒的同步處理。

條件變數的基本概念

條件變數是一種同步機制,允許執行緒等待特定的條件。它們在所有作業系統上都受到支援,但 API 可能有所不同。Delphi 的 System.SyncObjs 單元提供了 TConditionVariableCSTConditionVariableMutex 類別,但它們的實作方式在不同平台上有所不同。

自訂的條件變數實作

為了避免使用 Delphi 內建的條件變數類別的複雜性,本文採用了一個輕量級的自訂實作,名為 TLightweightCondVar。這個實作使用原生作業系統的條件變數支援,可以在所有平台上運作。

TLockConditionVariable 記錄

TLockConditionVariable 是一個記錄型別,結合了條件變數和鎖定機制(在 Windows 上使用 TLightweightMREW,在其他系統上使用 pthread_mutex_t)。它提供了同步功能(AcquireRelease)、訊號功能(SignalBroadcast)和等待功能(WaitTryWait)。

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;

主執行緒的實作

主執行緒可以使用 TLockConditionVariableWaitTryWait 方法來等待工作執行緒的完成。

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 的模式。