在 Delphi 多執行緒應用程式中,確保執行緒間安全地交換資料至關重要。本文將剖析幾種常用的執行緒通訊技術,包含同步與非同步方法,並探討它們的效能差異和適用場景。同步方法 TThread.Synchronize 能確保資料完整性,但可能造成效能瓶頸。非同步方法 TThread.Queue 則提升了效率,但需要額外處理資料一致性。訊息傳遞機制提供更靈活的通訊方式,適用於複雜的互動場景。而使用 TThreadedQueue<T> 結合定時器的輪詢機制則在效能和複雜度之間取得平衡。同時,文章也提供效能測試資料,並簡要介紹 Spring4D 和 OmniThreadLibrary 等第三方函式庫,為 Delphi 開發者在多執行緒程式設計中提供更全面的參考。

平行世界初探:同步與通訊的藝術

在平行程式設計的世界中,分享資料的管理是一項挑戰。當多個平行任務同時存取分享物件時,建立或銷毀該物件可能會引發問題。銷毀物件尤其危險,因為它可能導致其他執行緒存取已釋放的記憶體,從而引發程式當機。

銷毀分享物件的風險

在非ARC(自動參考計數)環境中,銷毀正在被其他執行緒使用的物件顯然是危險的。即使在ARC環境中,將分享介面設為nil也可能導致問題,因為一個執行緒可能會嘗試清除物件,而另一個執行緒卻試圖複製它。

FShared := nil; // 在一個執行緒中
tmp := FShared; // 在另一個執行緒中
tmp.DoSomething; // 可能會當機

這種情況可能會導致難以預料的行為,因為 _IntfClear_IntfCopy 的呼叫可能會相互幹擾。

建立分享物件的方法

與銷毀物件相比,建立物件則是可行的。透過使用鎖定機制,可以確保物件只被建立一次。

簡單鎖定法

一種方法是使用 TCriticalSection 來同步存取物件建立的過程。

// 在主執行緒中
FInitializationLock := TCriticalSection.Create;

// 在工作執行緒中
FInitializationLock.Acquire;
if not assigned(FSharedObject) then
  FSharedObject := TSharedObject.Create;
FInitializationLock.Release;

雙重檢查鎖定法

為了提高效率,可以使用雙重檢查鎖定模式。

// 在主執行緒中
FInitializationLock := TCriticalSection.Create;

// 在工作執行緒中
if not assigned(FSharedObject) then
begin
  FInitializationLock.Acquire;
  if not assigned(FSharedObject) then
    FSharedObject := TSharedObject.Create;
  FInitializationLock.Release;
end;

無鎖初始化法

甚至可以使用無鎖的技術,例如透過 TInterlocked.CompareExchange 來建立物件。

procedure CreateSharedObject;
var
  value: TSharedObject;
begin
  if not assigned(FSharedObject) then
  begin
    value := TSharedObject.Create;
    if TInterlocked.CompareExchange(pointer(FSharedObject), pointer(value), nil) <> nil then
      value.Free;
  end;
end;

內容解密:

此程式碼首先檢查 FSharedObject 是否已經被建立。如果沒有,它會建立一個新的 TSharedObject 例項並將其儲存在臨時變數 value 中。然後,它使用 CompareExchangevalue 儲存在分享變數 FSharedObject 中,但前提是 FSharedObject 仍然是 nil。如果另一個執行緒在此期間建立了物件並將其儲存在 FSharedObject 中,則 CompareExchange 將傳回非 nil 值,表示操作失敗,此時當前執行緒會釋放 value 以避免記憶體洩漏。

從分享到通訊

儘管資料分享很困難,但可以透過改變演算法來避免它。與其分享資料,不如複製資料並在各個執行緒中獨立處理,最後再將結果聚合起來。

資料複製與聚合的優勢

  1. 避免鎖定:減少了因鎖定而導致的效能瓶頸。
  2. 提高可擴充套件性:隨著CPU核心數量的增加,程式的效能可以更好地擴充套件。

這種方法的關鍵在於各個執行緒之間的通訊機制,使得主執行緒能夠收集並聚合各個執行緒的結果。

執行緒間的通訊:從工作執行緒傳送資料到主執行緒

在多執行緒程式設計中,將資料從一個執行緒傳送到另一個執行緒是一項基本且重要的任務。當我們不需要考慮物件所有權時,這個過程會變得更加簡單。最佳實踐是使用簡單的資料型別(例如整數)和受管理的資料型別(例如字串和介面)。由於這些資料不會同時在兩個執行緒中使用,因此物件生命週期的規則不適用。我們可以以正常的方式建立和銷毀這些物件。

傳送資料到主執行緒的方法

從工作執行緒向主執行緒傳送資料取決於主執行緒的實作方式。以下是四種與主執行緒通訊的技術:傳送 Windows 訊息、使用 SynchronizeQueue 和輪詢。

Windows 訊息

第一種方法是透過傳送 Windows 訊息給主執行緒。這個方法僅限於 Windows 作業系統。下面介紹兩種實作方式:第一種僅適用於 VCL 應用程式,而第二種也適用於 FireMonkey。

IncDecComm 程式中,所有的背景任務都以相同的方式啟動。當點選按鈕時,會呼叫 RunInParallel 方法,並傳遞兩個方法的名稱作為引數,一個用於遞增,另一個用於遞減初始值。

procedure TfrmIncDecComm.RunInParallel(task1, task2: TProc<integer>);
begin
  FNumDone := 0;
  FTasks[0] := TTask.Run(procedure begin task1(0); end);
  FTasks[1] := TTask.Run(procedure begin task2(0); end);
end;

使用 Windows 訊息的實作

當點選按鈕時,以下程式碼會被執行。它初始化結果 FValue 為零,以便正確聚合部分結果。同時,將 FNumDone 變數設為零,該變數用於計算完成任務的數量。

procedure TfrmIncDecComm.btnMessageClick(Sender: TObject);
begin
  FValue := 0;
  FNumDone := 0;
  RunInParallel(IncMessage, DecMessage);
end;

兩個任務的實作方式相同,下面只展示一個方法。IncMessage 方法將起始值遞增 CNumRepeat 次。最後,它會將 MSG_TASK_DONE 訊息回傳給主表單,並將結果值作為引數傳遞。

procedure TfrmIncDecComm.IncMessage(startValue: integer);
var
  i: integer;
  value: integer;
begin
  value := startValue;
  for i := 1 to CNumRepeat do
    value := value + 1;
  PostMessage(Handle, MSG_TASK_DONE, value, 0);
end;

處理 Windows 訊息

為了在主表單中處理這個訊息,我們需要宣告 MSG_TASK_DONE 變數,並為表單新增一個訊息處理方法 MsgTaskDone

const
  MSG_TASK_DONE = WM_USER;

procedure MsgTaskDone(var msg: TMessage);
message MSG_TASK_DONE;

這個方法會在每次主表單接收到 MSG_TASK_DONE 訊息時被呼叫。在內部,我們首先透過 msg.WParam 欄位傳遞的部分結果來遞增 FValue 值(聚合步驟)。之後,我們遞增 FNumDone 計數器。當 FNumDone 等於 2 時,我們呼叫清理方法 Done

procedure TfrmIncDecComm.MsgTaskDone(var msg: TMessage);
begin
  Inc(FValue, msg.WParam);
  Inc(FNumDone);
  if FNumDone = 2 then
    Done('Windows message');
end;

使用隱藏視窗處理 Windows 訊息

在 FireMonkey 中,我們需要建立一個隱藏視窗來接收和處理訊息。當點選 Message + AllocateHwnd 按鈕時,以下程式碼會被執行。它初始化 FValueFNumDone,然後透過呼叫 AllocateHwnd 建立一個隱藏視窗。

procedure TfrmIncDecComm.btnAllocateHwndClick(Sender: TObject);
begin
  FValue := 0;
  FNumDone := 0;
  FMsgWnd := AllocateHwnd(MsgWndProc);
  Assert(FMsgWnd <> 0);
  RunInParallel(IncMsgHwnd, DecMsgHwnd);
end;

清理隱藏視窗

為了清理這個隱藏視窗,我們需要呼叫 DeallocateHwnd 方法。

if FMsgWnd <> 0 then
begin
  DeallocateHwnd(FMsgWnd);
  FMsgWnd := 0;
end;

Synchronize 和 Queue

第二和第三種方法是相似的,因此將它們合併在一個章節中討論。

設定與第一種方法相同,我們只需要初始化 FValueFNumTasks。遞增和遞減任務中的程式碼也以相同的 for 迴圈開始,但隨後使用不同的方法將值傳回主執行緒。

使用 TThread.Synchronize

以下程式碼使用 TThread.Synchronize 在目標執行緒中執行匿名方法。Synchronize 方法接受一個 TThread 物件作為第一個引數,匿名方法作為第二個引數。它將在主執行緒中執行第二個引數。

procedure TfrmIncDecComm.IncSynchronize(startValue: integer);
var
  i: integer;
  value: integer;
begin
  value := startValue;
  for i := 1 to CNumRepeat do
    value := value + 1;
  TThread.Synchronize(nil,
    procedure
    begin
      // 在主執行緒中執行的程式碼
    end);
end;

圖表翻譯:

此圖示展示了使用Windows訊息與TThread.Synchronize進行執行緒間通訊的流程。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 圖表翻譯:

rectangle "傳送Windows訊息" as node1
rectangle "使用TThread.Synchronize" as node2

node1 --> node2

@enduml

圖表翻譯: 此圖示顯示了兩種不同的執行緒間通訊方法,分別是透過傳送Windows訊息和使用TThread.Synchronize,兩者都實作了從工作執行緒到主執行緒的資料傳輸。

平行世界入門:執行緒通訊技術詳解

在多執行緒程式設計中,如何有效地進行執行緒間的通訊是一個至關重要的課題。本文將探討 Delphi 中多種執行緒通訊的方法,包括 TThread.SynchronizeTThread.Queue、訊息傳遞機制以及使用 TThreadedQueue<T> 進行輪詢等技術。

使用 TThread.Synchronize 進行執行緒同步

TThread.Synchronize 是一種在 Delphi 中常用的執行緒同步方法。它允許在工作執行緒中暫停執行,並將指定的匿名方法傳遞到主執行緒中執行,然後等待主執行緒完成該方法的執行後再繼續工作執行緒。

程式碼範例

procedure TfrmIncDecComm.btnThSyncClick(Sender: TObject);
begin
  FValue := 0;
  FNumDone := 0;
  RunInParallel(IncThSync, DecThSync);
end;

procedure TfrmIncDecComm.IncThSync(startValue: integer);
var
  i: integer;
  value: integer;
begin
  value := startValue;
  for i := 1 to CNumRepeat do
    value := value + 1;
  TThread.Synchronize(nil,
    procedure
    begin
      PartialResult(value);
    end);
end;

procedure TfrmIncDecComm.PartialResult(value: integer);
begin
  Inc(FValue, value);
  Inc(FNumDone);
  if FNumDone = 2 then
    Done('Synchronize');
end;

內容解密:

  1. TThread.Synchronize 方法的使用:在工作執行緒中呼叫 TThread.Synchronize,將 PartialResult 方法傳遞到主執行緒中執行。這確保了 UI 更新和分享變數的存取是在主執行緒中進行的,避免了多執行緒競爭條件。
  2. PartialResult 方法的作用:更新分享變數 FValue 和任務完成計數器 FNumDone。當兩個任務都完成時,呼叫 Done 方法結束操作。

使用 TThread.Queue 進行非同步執行

TThread.Synchronize 不同,TThread.Queue 不會阻塞工作執行緒,而是將匿名方法排入主執行緒的佇列中並立即傳回,讓工作執行緒繼續執行後續任務。

程式碼範例

procedure TfrmIncDecComm.IncThQueue(startValue: integer);
var
  i: integer;
  value: integer;
begin
  value := startValue;
  for i := 1 to CNumRepeat do
    value := value + 1;
  TThread.Queue(nil,
    procedure
    begin
      PartialResult(value);
    end);
end;

內容解密:

  1. TThread.Queue 的非同步特性:與 Synchronize 不同,Queue 不會等待主執行緒完成匿名方法的執行,而是立即傳回,使工作執行緒能夠繼續處理其他任務,提高了整體效率。
  2. 適用場景:當工作執行緒不需要等待主執行緒處理結果時,使用 TThread.Queue 可以減少執行緒阻塞,提升系統效能。

輪詢機制:使用 TThreadedQueue<T>TTimer

另一種執行緒通訊的方式是透過輪詢機制,即主執行緒定期檢查佇列中是否有新的資料。

程式碼範例

procedure TfrmIncDecComm.btnThQueueAndTImerClick(Sender: TObject);
begin
  FValue := 0;
  FNumDone := 0;
  FResultQueue := TThreadedQueue<integer>.Create(2, 0, 0);
  RunInParallel(IncThQueue, DecThQueue);
  TimerCheckQueue.Enabled := true;
end;

procedure TfrmIncDecComm.IncThQueue(startValue: integer);
var
  i: integer;
  value: integer;
begin
  value := startValue;
  for i := 1 to CNumRepeat do
    value := value + 1;
  Assert(FResultQueue.PushItem(value) = wrSignaled);
end;

procedure TfrmIncDecComm.TimerCheckQueueTimer(Sender: TObject);
var
  qsize: integer;
  value: integer;
begin
  while FResultQueue.PopItem(qsize, value) = wrSignaled do
  begin
    FValue := FValue + value;
    Inc(FNumDone);
    if FNumDone = 2 then
    begin
      Done('TThreadedQueue + TTimer');
      break;
    end;
  end;
end;

圖表翻譯:

此圖示展示了使用 TThreadedQueue<T>TTimer 的輪詢機制。工作執行緒將計算結果推播到佇列中,而主執行緒透過定時器定期檢查佇列並處理資料。

  • 主執行緒啟動定時器並定期檢查佇列狀態。
  • 工作執行緒完成計算後將結果推播到佇列中。
  • 主執行緒從佇列中取出資料並進行匯總處理。

各種通訊方式的效能比較

方法時間 (毫秒)
原始單執行緒75
Critical Section1,017
TMonitor700
Spinlock493
TInterlocked407
Message-based16-23
Synchronize/Queue13-14
Timer-based24

圖表翻譯:

此圖示比較了不同執行緒通訊方法的效能。可以看出,根據訊息傳遞和佇列的方法明顯優於傳統的鎖定機制,其中 SynchronizeQueue 的效能最佳。

第三方函式庫支援

除了 Delphi 原生的功能外,還有多個第三方函式庫可以簡化平行程式設計,如 Spring4D 和 OmniThreadLibrary。這些函式庫提供了更豐富的功能和更簡潔的 API,能夠進一步提升開發效率。