WaitableTimer 與傳統同步物件 (Event/Mutex) 本質不同:它是**高精度定時器**,核心價值在於 **APC (非同步程序呼叫)** 回調機制。本文深度解析 APC 原理、為何需要特定等待函數、以及如何將定時器移至工作線程避免阻塞主線程。
| 特性 | TTimer (WM_TIMER) | WaitableTimer |
|---|---|---|
| 精度 | ~10-55ms (訊息佇列最低優先級) | 0.1ms (100ns 單位) |
| 機制 | 視窗訊息 | 核心物件 + APC 隊列 |
| 跨線程 | 困難 (需 PostMessage) | 原生支援 |
| 跨進程 | 不支援 | 支援 (具名) |
| 阻塞主線程 | 否 | 否 (配合工作線程) |
SetWaitableTimer 到期 → 將回調排入**目標線程** APC 隊列。SleepEx(..., True) 等) 才會執行 APC。procedure TimerAPCProc(
lpArgToCompletionRoutine: Pointer; // 用戶指標
dwTimerLowValue: DWORD; // 觸發時間 (UTC FILETIME 低位)
dwTimerHighValue: DWORD // 觸發時間 (UTC FILETIME 高位)
); stdcall;
只有以下函數能讓線程進入可喚醒等待狀態,允許 APC 執行:
SleepEx(dwMilliseconds, bAlertable: BOOL)WaitForSingleObjectEx(hHandle, dwMilliseconds, bAlertable: BOOL)WaitForMultipleObjectsEx(...)MsgWaitForMultipleObjectsEx(...)SignalObjectAndWait(...)bAlertable = True 時:有 APC 排隊 → 立即執行 APC 並返回 WAIT_IO_COMPLETION;無 APC → 正常等待超時或物件觸發。
var hTimer: THandle;
procedure TimerAPCProc(lpArg: Pointer; dwLow, dwHigh: DWORD); stdcall;
begin
Form1.Caption := IntToStr(StrToIntDef(Form1.Caption, 0) + 1); // 計數器
SleepEx(INFINITE, True); // 關鍵:重新進入可喚醒等待
end;
procedure TForm1.Button1Click(Sender: TObject);
var DueTime: Int64;
begin
hTimer := CreateWaitableTimer(nil, True, nil);
DueTime := 0;
if SetWaitableTimer(hTimer, DueTime, 0, @TimerAPCProc, nil, False) then
SleepEx(INFINITE, True); // 主線程卡死在此!
end;
unit Unit1;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls;
type TForm1 = class(TForm)
Button1, Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;
var Form1: TForm1;
implementation {$R *.dfm}
var hTimer: THandle;
procedure TimerAPCProc(lpArg: Pointer; dwLow, dwHigh: DWORD); stdcall;
begin
Form1.Caption := IntToStr(StrToIntDef(Form1.Caption, 0) + 1);
SleepEx(INFINITE, True); // APC 內再次等待,形成週期
end;
function MyThreadFun(p: Pointer): Integer; stdcall;
var DueTime: Int64;
begin
DueTime := 0;
{ 線程內建立/設定定時器 + 進入可喚醒等待迴圈 }
if SetWaitableTimer(hTimer, DueTime, 1000, @TimerAPCProc, nil, False) then
SleepEx(INFINITE, True);
Result := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var ID: DWORD;
begin
if hTimer = 0 then hTimer := CreateWaitableTimer(nil, True, nil);
CreateThread(nil, 0, @MyThreadFun, nil, 0, ID); // 啟動工作線程
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
CancelWaitableTimer(hTimer); // 取消定時器
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(hTimer);
end;
end.


object Form1: TForm1
Caption = 'Form1'
object Button1: TButton Caption = '啟動定時器' OnClick = Button1Click
object Button2: TButton Caption = '取消定時器' OnClick = Button2Click
end
procedure TimerAPCProc(...); stdcall;
begin
// 執行定時任務
DoWork;
// 關鍵:重新進入可喚醒等待,等待下一次定時器觸發
SleepEx(INFINITE, True);
end;
每次 APC 執行完畢 → 返回 SleepEx → 進入可喚醒等待 → 定時器到期 → 排隊 APC → 執行... 形成完美週期。
CancelWaitableTimer(hTimer):取消排程,已排隊的 APC 仍會執行一次。CloseHandle(hTimer):釋放核心物件,需在確定無 APC 執行後調用。CreateWaitableTimer / SetWaitableTimer / SleepEx **同一線程**。SleepEx(INFINITE, True) 參數正確 (無限等待 + 可喚醒)。stdcall、參數順序正確。WaitableTimer + APC 是 Windows 平台實現高精度、非阻塞、跨線程定時任務的**原生最佳方案**,遠優於輪詢迴圈或 WM_TIMER。