想過沒有? WaitableTimer 是在 "定時等待", 前面例子中的 WaitForSingleObject 等待函數 "也在等待", 這就 "雙重等待" 了, 這不好, 太浪費資源.
其實作為同步工具, 前面的幾種方法(事件、信號、臨界區)基本夠用了; WaitableTimer 的作用並不是為了重復前面的功能, 它的主要功用類似 TTimer 類; 譬如每隔多長時間執行一段代碼、或在指定的時間去執行一段代碼.
既然有了方便的 TTimer, 何必再使用 WaitableTimer 呢?
因為 WaitableTimer 比 TTimer 精確的多, 它的間隔時間可以精確到毫秒、它的指定時間甚至是精確到 0.1 毫秒;
而 TTimer 驅動的 WM_TIMER 消息, 是消息隊列中優先級最低的, 也就是再同一時刻 WM_TIMER 消息總是被最後處理.
還有重要的一點 WaitableTimer 可以跨線程、跨進程使用.
繼續探討一個重要的點: 很多時候為了讓線程不沖突, 線程也在等待, 既然有等待, 那 WaitableTimer 非常精確的定時又有什麼價值呢? 對這個問題的思考, 可以讓我們很好地理解 APC 函數.
SetWaitableTimer 有個回調函數(其實是個過程), Windows 要求它的格式是:
procedure TimerAPCProc(
lpArgToCompletionRoutine: Pointer;
dwTimerLowValue: DWORD;
dwTimerHighValue: DWORD
); stdcall;
函數名中有 APC 的字樣, 指示這是個 APC 函數(盡管這個名稱無所謂, 這是官方命名), 那什麼是 APC 函數?
APC(Asyncroneus Procedure Call): 異步過程調用.
原來每個線程除了有單獨的消息隊列, 還有一個 APC 隊列(等待執行的 APC 函數); 如果線程發現 APC 隊列中有情況, 馬上會跳過去執行, 執行完畢後才回來接著處理消息隊列.
說起來麻煩, 使用的時候只按上面格式傳入函數指針就行; 不過能進入 APC 隊列的回調函數和其他回調函數還有一個很大的不同:
SetWaitableTimer 按格式調用 APC 函數後, 需要在 "當前線程" 見到一個 "等待", 此 APC 函數才可以進入隊列.
這好像很費解, 例說一下: APC 隊列有那麼高的優先級, 因為對資源的優先使用會對其他消息有很大的影響, 肯定不能隨便進入, 這是不是像生活中的貴賓席或貴賓通道?
也就是說, 要進入 APC 隊列只有 SetWaitableTimer 的調用還不夠, 還要通過 "等待函數" 介紹一下.
WaitForSingleObject 嗎? 不是, 它不夠級別; 下面是 Windows 認可的、可以介紹 APC 入列的等待函數:
SleepEx();
WaitForSingleObjectEx();
WaitForMultipleObjectsEx();
MsgWaitForMultipleObjectsEx();
SignalObjectAndWait();
為什麼是用等待函數來把關? 因為上面幾個等待函數也可以等待是否有 APC 函數想入列.
上面給出的幾個等待函數, 就 SleepEx 的參數最少, 先用它吧:
function SleepEx(
dwMilliseconds: DWORD; {毫秒數}
bAlertable: BOOL {布爾值}
): DWORD; stdcall;
//第一個參數和 Sleep 的那個參數是一樣的, 是線程等待(或叫掛起)的時間, 時間一到不管後面參數如何都會返回.
//第二個參數如果是 False, SleepEx 將不會關照 APC 函數是否入列;
//若是 True, 只要有 APC 函數申請, SleepEx 不管第一個參數如何都會把 APC 推入隊列並隨 APC 函數一起返回.
//注意: SetWaitableTimer 和 SleepEx 必須在同一個線程才可以.
本例效果圖:
代碼文件:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
var
hTimer: THandle;
{APC 函數(過程), 函數名和參數名可以不同, 格式必須如此}
procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
dwTimerHighValue: DWORD); stdcall;
begin
Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1); {標題 + 1}
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
begin
SleepEx(INFINITE, True); {INFINITE 表示一直等}
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(hTimer);
end;
end.
窗體文件:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 113
ClientWidth = 203
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 64
Top = 48
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click
end
end
在上面例子中, 每點一次鼠標, 那個回調函數才執行一次; 作為定時器, 如果想讓它每秒執行一次怎麼弄?
但每一次執行那個 APC 函數, 都得有 SleepEx(當然不止它)給送進去, 那這樣得反復調用 SleepEx 才可以.
怎麼調用, 用循環嗎? 別說網上能找到的例子我沒見到不用循環的(太笨了), 就在那個 APC 函數裡調用不就完了.
當然這時一般要設時間間隔的, 下面我們將設間隔為 1000(1秒).
但接著問題又來了, 譬如把代碼修改成:
var
hTimer: THandle;
procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
dwTimerHighValue: DWORD); stdcall;
begin
Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1);
SleepEx(INFINITE, True); {這裡再次調用 SleepEx}
end;
procedure TForm1.Button1Click(Sender: TObject);
var
DueTime: Int64;
begin
hTimer := CreateWaitableTimer(nil, True, nil);
DueTime := 0;
{下面的參數 1000 表示間隔 1秒}
if SetWaitableTimer(hTimer, DueTime, 1000, @TimerAPCProc, nil, False) then
begin
SleepEx(INFINITE, True);
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CloseHandle(hTimer);
end;
任務能完成, 但窗體"死"了... 怎麼辦? 嘿, 現在學的不是多線程嗎?
下面例子中, 同時使用了 CancelWaitableTimer 來取消定時器, 很好理解;效果圖:
代碼文件:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
var
hTimer: THandle;
{APC 函數}
procedure TimerAPCProc(lpArgToCompletionRoutine: Pointer; dwTimerLowValue: DWORD;
dwTimerHighValue: DWORD); stdcall;
begin
Form1.Text := IntToStr(StrToIntDef(Form1.Text, 0) + 1);
SleepEx(INFINITE, True);
end;
{線程入口函數}
function MyThreadFun(p: Pointer): Integer; stdcall;
var
DueTime: Int64;
begin
DueTime := 0;
{SetWaitableTimer 必須與 SleepEx 在同一線程}
if SetWaitableTimer(hTimer, DueTime, 1000, @TimerAPCProc, nil, False) then
begin
SleepEx(INFINITE, True);
end;
Result := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ID: DWORD;
begin
{建立 WaitableTimer 對象}
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
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 113
ClientWidth = 203
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 55
Top = 32
Width = 97
Height = 25
Caption = #21551#21160#23450#26102#22120
TabOrder = 0
OnClick = Button1Click
end
object Button2: TButton
Left = 55
Top = 63
Width = 97
Height = 25
Caption = #21462#28040#23450#26102#22120
TabOrder = 1
OnClick = Button2Click
end
end
使用 APC 回調函數才是 WaitableTimer 的正途, 下次該是如何給這個函數傳遞參數了.