可等待定时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次。它们通常用来在某个时间执行一些操作。
CreateWaitableTimer函数用于创建一个可等待定时器:
HANDLE WINAPI CreateWaitableTimer(
__in_opt LPSECURITY_ATTRIBUTES lpTimerAttributes, //安全描述符,为NULL时使用默认的
//且该句柄不被子进程继承
__in BOOL bManualReset, //要创建一个手动重置定时器还是一个自动重置计时器
//当手动重置计时器被触发时,正在等待该计时器的所有线程都会变成可调度状态
//当自动重置计时器被触发时,只有一个正在等待该计时的线程会变成可调度状态
__in_opt LPCTSTR lpTimerName //该可等待计时器的名称
);
函数OpenWaitableTimer用于得到一个已经存在的可等待计时器的句柄,该句柄与当前进程相关联:
HANDLE WINAPI OpenWaitableTimer(
__in DWORD dwDesiredAccess, //访问权限
__in BOOL bInheritHandle, //是否允许子进程继承该句柄
__in LPCTSTR lpTimerName //要打开的对象名称
);
在创建的时候,可等待的计时器对象总是处于未触发状态。当我们想要触发计时器时,必须调用SetWaitableTimer函数:
BOOL WINAPI SetWaitableTimer(
__in HANDLE hTimer, //想要触发的计时器
__in const LARGE_INTEGER *pDueTime, //计时器第一次触发的时间
__in LONG lPeriod, //第一次触发后,计时器的触发频度
__in_opt PTIMERAPCROUTINE pfnCompletionRoutine, //异步过程调用APC函数
__in_opt LPVOID lpArgToCompletionRoutine, //APC函数的参数
__in BOOL fResume //是否继续执行,一般传FALSE
);
函数CancelWaitableTimer用来将指定的计时器取消:
BOOL WINAPI CancelWaitableTimer(
__in HANDLE hTimer
);
这样计时器就永远不会触发了,除非以后再调用SetWaitableTimer来对它进行重置。如果想要改变触发器的触发时间,不必先调用CancelWaitableTimer,因为每次调用SetWaitableTimer都会在设置新的触发时间之前将原来的触发时间取消掉。
下面代码把计时器的第一次触发时间设置为2011年1月1日下午1:00,之后每隔6小时触发一次:
//声明局部变量
HANDLE hTimer;
SYSTEMTIME st;
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC;
//创建自动重置定时器
hTime = CreateWaitableTime(NULL, FALSE, NULL);
//设置第一次触发时间,这是本地时间
st.wYear = 2011;
st.wMonth = 1;
st.wDayOfWeek = 0; //忽略
st.wDay = 1;
st.wHout = 13;
st.wMinute = 0;
st.wSecond = 0;
st.wMilliseconds = 0;
SystemTimeToFileTime(&st, &ftLocal);
//将本地时间转为UTC时间(因为SetWaitableTimer函数传入的时间始终是全球标准时间)
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
//虽然FILETIME结构和LARGE_INTEGER结构的二进制格式完全相同
//但它们的结构对齐方式不同(FILETIME对齐到32位边界,LARGE_INTEGER
//对齐到64位边界),因此不能强制转换
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;
//设置定时器
SetWaitableTimer(hTimer, &liUTC, 6*60*60*1000,
NULL, NULL, FALSE);
...
在上面代码中,我们给定时器第一次触发时间指定的是绝对时间,也可以指定一个相对时间,只要将pDueTime参数中传入一个负值,且传入的值必须是100纳秒的整数倍。换算关系如下:
1秒 = 1000毫秒 = 1000 000微妙 = 10 000 000个100纳秒
下面代码把计时器第一次触发时间设置为SetWaitableTimer函数调用结束后的10秒钟:
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <stdio.h>
int main()
{
HANDLE hTimer = NULL;
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = -100000000LL; //10秒,以100纳秒为单位
// Create an unnamed waitable timer.
hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
if (NULL == hTimer)
{
printf("CreateWaitableTimer failed (%d)/n", GetLastError());
return 1;
}
printf("Waiting for 10 seconds.../n");
// Set a timer to wait for 10 seconds.
if (!SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, 0))
{
printf("SetWaitableTimer failed (%d)/n", GetLastError());
return 2;
}
// Wait for the timer.
if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0)
printf("WaitForSingleObject failed (%d)/n", GetLastError());
else printf("Timer was signaled./n");
return 0;
}
当给lPeriod参数传0时,我们设置的是一次性定时器,这种定时器只触发一次,之后再不触发。
当计时器被触发时,Microsoft允许计时器把一个异步过程调用(asynchronous procedure call, APC)放到SetWaitableTimer的调用线程的队列中。调用SetWaitableTimer时,给pfnCompletionRoutine和pvArgToCompletionRoutine两个参数传NULL时,SetWaitableTimer知道时间一到应该触发计时器对象;但是如果希望时间一到就让计时器把一个APC添加到队列中,就必须实现一个计时器APC函数,并将其地址传入。APC函数原型如下:
VOID CALLBACK TimerAPCProc(
__in_opt LPVOID lpArgToCompletionRoutine, //同传给SetWaitableTimer的参数
__in DWORD dwTimerLowValue, //计时器被触发的时间
__in DWORD dwTimerHighValue //同上
);
计时器被触发的时候,当且仅当SetWaitableTimer的调用线程正处于可提醒状态时,这个APC函数会被同一线程调用。也就是说,线程必须是由于调用SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx或者SignalObjectAndWait而进入的等待状态。如果线程并非在其中的一个函数内等待,那么系统不会把计时器的APC函数添加到线程的队列中。
当计时器被触发时,如果线程处于可提醒状态,系统会让线程调用APC回调函数:
#define UNICODE 1
#define _UNICODE 1
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#define _SECOND 10000000
typedef struct _MYDATA {
TCHAR *szText;
DWORD dwValue;
} MYDATA;
VOID CALLBACK TimerAPCProc(
LPVOID lpArg, // Data value
DWORD dwTimerLowValue, // Timer low value
DWORD dwTimerHighValue ) // Timer high value
{
MYDATA *pMyData = (MYDATA *)lpArg;
_tprintf( TEXT("Message: %s/nValue: %d/n/n"), pMyData->szText,
pMyData->dwValue );
MessageBeep(0);
}
void main( void )
{
HANDLE hTimer;
BOOL bSuccess;
__int64 qwDueTime;
LARGE_INTEGER liDueTime;
MYDATA MyData;
MyData.szText = TEXT("This is my data");
MyData.dwValue = 100;
hTimer = CreateWaitableTimer(
NULL, // Default security attributes
FALSE, // Create auto-reset timer
TEXT("MyTimer")); // Name of waitable timer
if (hTimer != NULL)
{
__try
{
// Create an integer that will be used to signal the timer
// 5 seconds from now.
qwDueTime = -5 * _SECOND;
// Copy the relative time into a LARGE_INTEGER.
liDueTime.LowPart = (DWORD) ( qwDueTime & 0xFFFFFFFF );
liDueTime.HighPart = (LONG) ( qwDueTime >> 32 );
bSuccess = SetWaitableTimer(
hTimer, // Handle to the timer object
&liDueTime, // When timer will become signaled
2000, // Periodic timer interval of 2 seconds
TimerAPCProc, // Completion routine
&MyData, // Argument to the completion routine
FALSE ); // Do not restore a suspended system
if ( bSuccess )
{
for ( ; MyData.dwValue < 1000; MyData.dwValue += 100 )
{
SleepEx(
INFINITE, // Wait forever
TRUE ); // Put thread in an alertable state
}
}
else
{
printf("SetWaitableTimer failed with error %d/n", GetLastError());
}
}
__finally
{
CloseHandle( hTimer );
}
}
else
{
printf("CreateWaitableTimer failed with error %d/n", GetLastError());
}
}
最后需要注意的是:线程不应该在等待一个计时器句柄的同时以可提醒的方式等待同一个计时器,如:
HANDLE hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
SetWaitableTimer(hTimer, ..., TimerAPCRoutine, ...);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
因为对WaitForSingleObjectEx的调用实际上会等待计时器两次:一次是可提醒的,另一次是内核对象句柄。当计时器被触发时,等待成功,线程被唤醒,这使线程退出可提醒状态,APC函数没有被调用。
可等待计时器和用户计时器(通过SetTimer函数来设置)两者最大的区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外,可等待计时器是内核对象,这意味着它不仅可以在多个线程间共享,而且可以具备安全性。
用户计时器会产生WM_TIMER消息,这个消息被送回调用SetTimer的线程(对回调计时器来说),或者被送回创建窗口的线程(对基于窗口的计时器来说)。因此,当一个用户计时器触发时,只有一个线程会得到通知;相反,多个线程可以等待可等待计时器,如果计时器是手动重置计时器,那么计时器触发时,有多个线程可以变成可调度状态。