定時器觸發任務時,若上一輪任務尚未完成便再次觸發,會產生多執行緒競爭、資料錯亂。單例模式配合 FreeOnTerminate 與全域實例變數,可確保同一時間只有一個工作執行緒存在。本文展示 Windows 服務定時下發簡訊場景的完整實作。
TSendThread 執行耗時操作 (查詢資料庫、發送簡訊)。
unit Unit_Send;
interface
uses SysUtils, Classes, StrUtils, Main;
type
TSendThread = class(TThread)
public
constructor Create(CreateSuspended: Boolean);
destructor Destroy; override;
class function GetInstance: TSendThread;
protected
procedure Execute; override;
end;
var
Global_Instance: TSendThread; // 全域唯一實例
implementation
uses DB;
class function TSendThread.GetInstance: TSendThread;
begin
if Global_Instance = nil then
Global_Instance := TSendThread.Create(False); // 立即啟動
Result := Global_Instance;
end;
constructor TSendThread.Create(CreateSuspended: Boolean);
begin
if Global_Instance = nil then begin
inherited Create(CreateSuspended);
FreeOnTerminate := True; // 執行緒結束自動 Free
end else
raise Exception.Create('Cannot create more than one TSendThread instance!');
end;
destructor TSendThread.Destroy;
begin
inherited;
end;
procedure TSendThread.Execute;
var
User: TUserInfo;
SQL: String;
begin
// 1. 執行核心任務
SQL := 'SELECT * FROM ' + Main.MainUserTable + ' WHERE destroy=0 ORDER BY id';
Service1.ADOQuery_Send.Connection := Conn_Server;
SQLQuery(Service1.ADOQuery_Send, SQL);
while (not Service1.ADOQuery_Send.Eof) and (not Terminated) do begin
User.SeqID := '0';
User.UID := '';
User.Spc := GetMsg('PARAMETER', 'Spcode', TheExeName + '.ini');
User.RecordID := '0';
User.Mob := Service1.ADOQuery_Send.FieldByName('mobile').AsString;
AutoJoke(User); // 發送簡訊
Service1.ADOQuery_Send.Next;
end;
// 2. 任務完成後睡眠 60 秒,防止 Timer 立即再次觸發
Sleep(600001); // 10 分鐘 + 1 秒
// 3. 清除全域實例,允許下一輪建立
Global_Instance := nil;
Terminate;
end;
end.
procedure TService1.Timer_MainTimer(Sender: TObject);
var NowTime, TargetTime: String;
begin
try
DateTimeToString(NowTime, 'hh:nn', Now);
TargetTime := LeftStr(GetMsg('GAME', 'SendTime', TheExeName + '.ini'), 4); // HH:NN
if LeftStr(NowTime, 4) = TargetTime then begin
Global_Instance := TSendThread.GetInstance; // 獲取或建立單例
end;
except
on E: Exception do LogError('Timer建立執行緒失敗: ' + E.Message);
end;
end;
| 機制 | 作用 |
|---|---|
| Global_Instance 變數 | 進程級單一實例標誌 |
| GetInstance 類別方法 | 雙重檢查鎖定 (單執行緒環境下足夠) |
| Constructor 檢查 Global_Instance | 防止外部直接 Create 繞過單例 |
| FreeOnTerminate = True | 執行緒結束自動釋放物件記憶體 |
| Execute 結束前 Sleep + 清 nil | 拉開時間窗,避免 Timer 密集觸發 |
| Terminated 檢查 | 支援外部優雅取消 |
若 GetInstance 可能被多執行緒同時呼叫,需用 Critical Section 或 InterlockedCompareExchange:
class function TSendThread.GetInstance: TSendThread;
var Temp: TSendThread;
begin
if Global_Instance = nil then begin
Temp := TSendThread.Create(True); // 暫停建立
if InterlockedCompareExchange(Pointer(Global_Instance), Temp, nil) <> nil then
Temp.Free; // 另一執行緒搶先建立,丟棄新建實例
end;
Result := Global_Instance;
end;
| 方案 | 優點 | 缺點 |
|---|---|---|
| 單例執行緒 (本文) | 邏輯封裝完整、自動清理、支援優雅退出 | 需手動管理全域變數 |
| TEvent + 單一工作執行緒 | 標準生產者消費者模式、可排隊多任務 | 架構較複雜 |
| Timer 單次觸發 + 任務完成重啟 | 無全域狀態、簡單 | 任務超時會導致間隔變長、難以精確定時 |
| 資料庫鎖 / 檔案鎖 | 跨進程互斥 | 效能低、殘留鎖風險 |
單例執行緒模式最適合「定時觸發、單任務、執行時間不可預測、嚴禁並發」的業務場景,代碼清晰、維護容易。