在 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來同步化對物件的存取。與關鍵段類別似,但具有自動關聯到物件的優勢,並且支援更多進階功能,如TryEnter和Wait。
平行程式函式庫(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可能導致的問題。