首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> Delphi

Delphi 多線程程式設計 (16):WaitableTimer 等待定時器 - APC 回調機制深度解析與線程化實戰

     編輯:Delphi

WaitableTimer 與傳統同步物件 (Event/Mutex) 本質不同:它是**高精度定時器**,核心價值在於 **APC (非同步程序呼叫)** 回調機制。本文深度解析 APC 原理、為何需要特定等待函數、以及如何將定時器移至工作線程避免阻塞主線程。

為什麼需要 WaitableTimer?

特性TTimer (WM_TIMER)WaitableTimer
精度~10-55ms (訊息佇列最低優先級)0.1ms (100ns 單位)
機制視窗訊息核心物件 + APC 隊列
跨線程困難 (需 PostMessage)原生支援
跨進程不支援支援 (具名)
阻塞主線程否否 (配合工作線程)

APC (Asynchronous Procedure Call) 核心概念

  1. 每線程擁有 **APC 隊列** (獨立於訊息隊列)。
  2. APC 回調優先級 **高於** 訊息處理。
  3. SetWaitableTimer 到期 → 將回調排入**目標線程** APC 隊列。
  4. 線程**必須處於可喚醒等待** (SleepEx(..., True) 等) 才會執行 APC。

APC 回調標準原型

procedure TimerAPCProc(
  lpArgToCompletionRoutine: Pointer;  // 用戶指標
  dwTimerLowValue: DWORD;             // 觸發時間 (UTC FILETIME 低位)
  dwTimerHighValue: DWORD             // 觸發時間 (UTC FILETIME 高位)
); stdcall;

關鍵:可喚醒等待函數 (APC 入隊守門員)

只有以下函數能讓線程進入可喚醒等待狀態,允許 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.

執行效果

按鈕啟動計數器,標題每秒遞增

啟動/停止按鈕控制定時器,主線程完全不阻塞

DFM 定義

object Form1: TForm1
  Caption = 'Form1'
  object Button1: TButton Caption = '啟動定時器' OnClick = Button1Click
  object Button2: TButton Caption = '取消定時器' OnClick = Button2Click
end

APC 迴圈形成週期的關鍵模式

procedure TimerAPCProc(...); stdcall;
begin
  // 執行定時任務
  DoWork;
  // 關鍵:重新進入可喚醒等待,等待下一次定時器觸發
  SleepEx(INFINITE, True);
end;

每次 APC 執行完畢 → 返回 SleepEx → 進入可喚醒等待 → 定時器到期 → 排隊 APC → 執行... 形成完美週期。

進階:取消定時器與資源清理

  • CancelWaitableTimer(hTimer):取消排程,已排隊的 APC 仍會執行一次。
  • CloseHandle(hTimer):釋放核心物件,需在確定無 APC 執行後調用。
  • 工作線程結束時自動取消定時器 (核心物件隨進程清理),但顯式清理更佳。

實務開發棧查表

  1. ✅ CreateWaitableTimer / SetWaitableTimer / SleepEx **同一線程**。
  2. ✅ SleepEx(INFINITE, True) 參數正確 (無限等待 + 可喚醒)。
  3. ✅ APC 回調 stdcall、參數順序正確。
  4. ✅ 週期參數 (第 3 參數) 單位為**毫秒**,0 = 單次觸發。
  5. ✅ 指標參數 (第 5 參數) 生命週期覆蓋 APC 執行期間。
  6. ✅ 主線程 UI 完全不阻塞,所有等待在工作線程。

WaitableTimer + APC 是 Windows 平台實現高精度、非阻塞、跨線程定時任務的**原生最佳方案**,遠優於輪詢迴圈或 WM_TIMER。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved