在 Delphi 開發中,有效利用多執行緒技術對於提升應用程式效能和回應速度至關重要。本文將介紹如何使用 TThread 類別建立和管理執行緒,以及如何透過 Synchronize 方法確保執行緒安全地與主執行緒互動。同時,我們也將探討如何使用關鍵段(Critical Section)、原子操作、TMonitor 等機制來解決多執行緒存取同步的問題,並深入研究 Delphi 的平行程式函式庫(PPL),利用 TTask 和平行迴圈等特性簡化多執行緒程式開發,進一步提升程式效能。透過這些技術,開發者可以更有效地利用多核心處理器,開發更快速、更流暢的應用程式體驗。

使用執行緒提升應用程式效能

在現代多核心處理器的運算環境中,如何有效利用多執行緒技術來提升應用程式的效能與回應速度,已成為開發人員必須面對的重要課題。本文將探討在 Delphi 中使用 TThread 類別來建立和管理執行緒的基本方法,以及如何透過 Synchronize 方法安全地與主執行緒進行互動。

為何使用執行緒?

當作業系統啟動一個應用程式時,它會為該程式建立一個行程,並啟動一個主要的執行緒(通常稱為主執行緒)。在每個行程中,可以有多個執行緒同時執行,這在多核心處理器上尤為重要。這意味著多個執行緒可以在不同的核心上平行執行。傳統上,大多數應用程式只在一個執行緒中執行,僅利用單一處理器核心,其他核心則閒置。

然而,使用多執行緒有其多重好處。最重要的原因並非單純為了提升效能,而是為了在進行大量運算或等待外部資訊(如 HTTP 請求)時,保持應用程式的回應性。另一個常見的應用場景是利用多執行緒加速計算過程,儘管這通常伴隨著更高的複雜度。

在 Delphi 中建立執行緒

自 Delphi 早期版本以來,Runtime Library (RTL) 就提供了 TThread 類別,用於代表作業系統中的執行緒概念。此外,Delphi 也提供了一個簡單的精靈(wizard)來協助開發者快速上手執行緒程式設計。

使用 Thread Object 精靈

在 Delphi 中建立新應用程式後,可以透過 File | New | Other 選單開啟 New Items 對話方塊,選擇 Individual Files,然後選擇 Thread Object。這個精靈會引導你建立一個基本的執行緒類別。

此精靈生成的類別宣告如下:

type
  TMyThread = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

Execute 方法的初始實作只是一個佔位符:

procedure TMyThread.Execute;
begin
  { Place thread code here }
end;

建立和啟動執行緒

要建立並啟動一個執行緒,可以在主表單或其他適當的位置撰寫以下程式碼:

var
  AThread := TMyThread.Create;
begin
  AThread.FreeOnTerminate := True;
  AThread.Start;
end;

這裡使用了 TMyThread 的自訂建構函式。當然,也可以直接使用 TThread 類別。TThread 的建構函式有一個引數 CreateSuspended,用於決定執行緒是否立即啟動。如果需要在啟動前傳遞引數給執行緒,可以先暫停執行緒,待設定完成後再手動啟動。

執行緒中的資料處理

在範例應用程式中,我們將進行一些數值處理,生成一億個隨機數存入記憶體列表,並找出其中的最小值和最大值。以下是部分關鍵程式碼:

procedure TMyThread.FillList;
begin
  Randomize;
  CS.Acquire;
  try
    for var I := 1 to 100_000_000 do
      FIntList.Add(Random(MaxInt));
  finally
    CS.Release;
  end;
end;

procedure TMyThread.Execute;
var
  LMin, LMax: Integer;
begin
  FillList;
  LMin := MaxInt;
  for var I := 0 to FIntList.Count - 1 do
  begin
    if FIntList[I] < LMin then
      LMin := FIntList[I];
  end;
  
  #### 內容解密:
  這段程式碼首先呼叫 `FillList` 方法生成一億個隨機數並存入 `FIntList`。接著,在 `Execute` 方法中遍歷列表找出最小值 `LMin`。
  
  Synchronize(
    procedure
    begin
      FormThread.ShowMin(LMin);
    end);
end;

procedure TFormThread.ShowMin(AMin: Integer);
begin
  Label1.Text := 'Min: ' + AMin.ToString;
end;

與主執行緒同步

在多執行緒程式設計中,子執行緒無法直接存取 UI 元件,因為 UI 元件並非執行緒安全的。為瞭解決這個問題,Delphi 提供了 Synchronize 方法,讓子執行緒可以請求主執行緒代為執行某些程式碼,從而安全地更新 UI。

圖表說明

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Delphi多執行緒程式設計與效能提升

package "安全架構" {
    package "網路安全" {
        component [防火牆] as firewall
        component [WAF] as waf
        component [DDoS 防護] as ddos
    }

    package "身份認證" {
        component [OAuth 2.0] as oauth
        component [JWT Token] as jwt
        component [MFA] as mfa
    }

    package "資料安全" {
        component [加密傳輸 TLS] as tls
        component [資料加密] as encrypt
        component [金鑰管理] as kms
    }

    package "監控審計" {
        component [日誌收集] as log
        component [威脅偵測] as threat
        component [合規審計] as audit
    }
}

firewall --> waf : 過濾流量
waf --> oauth : 驗證身份
oauth --> jwt : 簽發憑證
jwt --> tls : 加密傳輸
tls --> encrypt : 資料保護
log --> threat : 異常分析
threat --> audit : 報告生成

@enduml
```delphi
procedure TMyThread.Execute;
begin
  // ...
  Synchronize(procedure
    begin
      // 在主執行緒中執行的程式碼
    end);
  // ...
end;

#### 內容解密: 此範例展示瞭如何使用匿名方法與Synchronize結合,確保特定程式碼在主執行緒中安全執行。這種方法簡單直接,但如果大量使用,可能會導致多執行緒的優勢喪失。

原子操作

另一種相對簡單的解決方案是使用原子操作,確保操作作為單一不可中斷的程式碼執行。Delphi的RTL提供了TInterlocked類別來支援核心數學運算的原子操作。

TInterlocked.Increment(FCount);

#### 內容解密: 此範例展示瞭如何使用TInterlocked.Increment進行原子增量操作。對於需要在多個執行緒中對變數進行操作的情況,這是一種簡單有效的解決方案。

使用關鍵段(Critical Section)

關鍵段是一種保護程式碼區塊的機制,確保同一時間只有一個執行緒可以執行該區塊。Delphi提供了TCriticalSection類別來實作關鍵段。

var
  CS: TCriticalSection;

initialization
  CS := TCriticalSection.Create;
finalization
  CS.Free;

// 在執行緒中使用關鍵段
begin
  CS.Acquire;
  try
    // 需要保護的程式碼
  finally
    CS.Release;
  end;
end;

#### 內容解密: 此範例展示瞭如何使用TCriticalSection來保護關鍵程式碼區塊。務必使用try/finally區塊確保在發生例外時能夠釋放關鍵段,避免程式被鎖死。

使用TMonitor

TMonitor是Delphi RTL中的一個核心機制,每個物件都有一個關聯的存取鎖,可以用來獲得對該物件的獨佔存取許可權。

System.TMonitor.Enter(FIntList);
try
  // 使用FIntList
finally
  System.TMonitor.Exit(FIntList);
end;

#### 內容解密: 此範例展示瞭如何使用TMonitor來同步化對物件的存取。與關鍵段類別似,但具有自動關聯到物件的優勢,並且支援更多進階功能,如TryEnterWait

平行程式函式庫(Parallel Programming Library, PPL)

Delphi提供了更高階的抽象——PPL,讓開發者可以更專注於應用邏輯,而不用過度關注底層硬體和CPU能力。PPL的核心元素是TTask類別,概念上類別似於執行緒,但更為抽象化和高效。

TTask.Run(procedure
  begin
    // 非同步執行的任務
  end);

#### 內容解密: 此範例展示瞭如何使用TTask.Run來建立一個非同步任務。PPL會根據CPU的多核心能力自動管理執行緒池,並分配任務給這些執行緒,從而提高效率並減少執行緒建立的負擔。

深入探索平行程式函式庫(Parallel Programming Library)

要使用平行程式函式庫(PPL),首先需要在程式的 uses 子句中加入 System.Threading 單元。如前所述,PPL會在應用程式內部維護一個自動調節的執行緒池,用於執行各項任務。

平行迴圈

PPL中最容易理解的概念是平行 for 迴圈。當控制變數的不同值之間的計算是獨立的,且執行順序並不重要時,這種迴圈非常有用。電腦圖形學中的光線追蹤演算法就是一個很好的例子。為了生成影像,我們需要計算構成最終影像的每個畫素的顏色。這是透過計算光線在空間中的路徑來完成的。計算給定畫素的顏色完全獨立於其他畫素,可以同時進行,以更快地生成最終的點陣圖。

簡單的平行迴圈範例

我們來建立一個簡單的範例,以觀察平行迴圈比傳統迴圈執行得快多少。我們將在迴圈的每次迭代中呼叫 Sleep 程式,使當前執行緒暫停一段時間。

首先,建立一個新的多裝置專案,並在表單上放置兩個按鈕。然後,撰寫以下程式碼;每個按鈕的標題將顯示迴圈執行的時間。為了計算經過的時間,我們使用 System.Diagnostics 單元中的 TStopWatch 記錄型別。

function TForm1.DoTimeConsumingOperation(Length: integer): Double;
begin
  var Tot := 1.0;
  for var I := 1 to 10_000 * Length do
    // 一些隨機的慢速數學運算
    Tot := Log2(Tot) + Sqrt(I);
  Result := Tot;
end;

傳統迴圈與平行迴圈的比較

以下是使用傳統 for 迴圈的程式碼:

procedure TForm1.btnForLoopRegularClick(Sender: TObject);
var
  SW: TStopwatch;
begin
  SW := TStopwatch.StartNew;
  for var I := 0 to 99 do
    DoTimeConsumingOperation(10);
  SW.Stop;
  (Sender as TButton).Text := SW.ElapsedMilliseconds.ToString + 'ms';
end;

平行迴圈實作

以下是使用平行 for 迴圈的相同程式碼:

procedure TForm1.btnForLoopParallelClick(Sender: TObject);
var
  SW: TStopwatch;
  I: Integer;
begin
  SW := TStopwatch.StartNew;
  TParallel.For(0, 99, procedure(I: integer)
    begin
      DoTimeConsumingOperation(10);
    end);
  SW.Stop;
  (Sender as TButton).Text := SW.ElapsedMilliseconds.ToString + 'ms';
end;

#### 內容解密:

  • TParallel.For 是 PPL 提供的方法,用於建立平行迴圈。
  • 它接受起始索引、結束索引和一個匿名程式作為引數。
  • 在這個範例中,匿名程式呼叫 DoTimeConsumingOperation(10),模擬耗時的運算。

使用任務(Tasks)

多執行緒程式設計的一個關鍵應用場景是,在執行長時間運算的同時,保持使用者介面的回應性。主要的應用程式執行緒(即使用者介面執行的執行緒)不應該被耗時的操作佔用。這些操作應該在背景執行緒中執行,以保持使用者介面的回應性。

範例:非回應式與回應式按鈕點選事件

procedure TForm1.btnNonResponsiveClick(Sender: TObject);
begin
  DoTimeConsumingOperation(3_000);
  (Sender as TButton).Text := 'Done';
end;

procedure TForm1.btnResponsive1Click(Sender: TObject);
var
  ATask: ITask;
begin
  ATask := TTask.Create(procedure
    begin
      DoTimeConsumingOperation(3_000);
    end);
  ATask.Start;
  (Sender as TButton).Text := 'Done';
end;

#### 內容解密:

  • 第一個按鈕點選事件直接呼叫 DoTimeConsumingOperation(3_000),導致表單凍結幾秒鐘。
  • 第二個按鈕點選事件建立一個任務,在背景執行緒中執行耗時的操作,保持了表單的回應性。
  • TTask.Create 用於建立一個新任務,ATask.Start 用於啟動任務。

同步更新UI

如果需要在任務完成後更新UI,需要使用 TThread.Synchronize 方法,因為UI操作必須在主執行緒中進行。

procedure TForm1.btnResponsive2Click(Sender: TObject);
var
  ATask: ITask;
begin
  ATask := TTask.Create(procedure
    begin
      DoTimeConsumingOperation(3000);
      TThread.Synchronize(nil,
        procedure
        begin
          (Sender as TButton).Text := 'Done';
        end);
    end);
  ATask.Start;
end;

#### 內容解密:

  • 在任務內部,使用 TThread.Synchronize 將UI更新操作同步到主執行緒。
  • 這確保了UI元件的安全更新,避免了跨執行緒操作UI可能導致的問題。