通过异步程序调用(APC)实现的定时功能
定时器是一个在特定时间或者规则间隔被激发的内核对象。结合定时器的异步程序调用可以允许回调函数在任何定时器被激发的时候执行。本文的例子代码显示了如何实现。
使用本定时器时,你需要把常量_WIN32_WINNT定义为0x0400,并且此常量应该在包
通过调用CreateWaitableTimer()可以创建一个定时器,此函数返回一个指向内核对象的句柄。若定时器已经存在,你可以通过使用OpenWaitableTimer()获得一个进程相关的句柄。无论是通过CreateWaitableTimer() 还是通过OpenWaitableTimer()获得的句柄,在不需要定 时器时必须释放,方法是使用函数CloseHandle()。
定时的时间通过调用SetWaitableTimer()来设置,可以设置为一个特定的时刻(如December 16, 1999 at 9:45 PM)或者一个相对的时间(如从现在起每五分钟)。函数SetWaitableTime()定时的时间参数要求LARGE_INTEGER类型。这个值应该符合在结构体FILETIME中描述的格式。如果值是正的,代表一个特定的时刻。如果值是负的,代表以100纳秒为单位的相对时间。后面的示例代码中使用的是相对时间。在调用SetWaitableTimer()函数后,定时器将在每5秒被激发一次。
你也可以将定时器设置为周期性的自我激发,方法是向SetWaitableTimer()的第三个参数传递一个周期参数(以毫秒为单位)。在CreateWaitableTimer()的第二个参数传递FALSE可以产生一个自动归零的定时器。本例设置周期为两秒的定时器。
当设置了定时器之后,你就可以将APC与其结合起来。这里把APC函数称作完全例程。完全例程的地址作为SetWaitableTimer()的第四个参数。第五个参数是一个空类型的指针,你可以使用它来传递完全例程的参数。
在所有的APC中,要执行一个完全例程则线程必须处于监听状态。完全例程将总是被调用SetWaitableTimer()的相同的线程执行,所以此线程必须将必须其自身置于监听状态。可以调用下面的任何一个监听函数来完成监听状态的设置:
- SleepEx();
- WaitForSingleObjectEx();
- WaitForMultipleObjectsEx();
- MsgWaitForMultipleObjectsEx();
- SignalObjectAndWait();
任何一个线程都有一个APC队列。在调用上面的任何一个函数时,如果线程的APC队列中有实体,则此线程不会进入休眠状态,取而代之要做的是将实体从APC队列中取出,然后调用相应的完全例程。
如果在APC队列中不存在实体,那么线程将会被挂起,直至等待条件满足为止。满足等待条件的有:一个实体加入到APC队列中,超时,激活句柄等,以及在调用MsgWaitForMultipleObjectsEx()情况下,一个消息进入到线程的一个消息队列中。若等待条件满足的是APC队列中的一个实体,那么线程会被激活,并且执行完全例程,这种情况下的函数的返回值是 WAIT_IO_COMPLETION.
【重要提示】
1、在执行完一个完全例程之后,系统会检查在APC中剩下的实体以处理。一个监视函数仅仅在处理完所有APC实体后才返回。因此,如果实体加入到APC队列的速度比处理的更快的话,则调用这些函数可能永远也不能返回。特别当定时等待的时间比起要求执行完全例程的时间更短的话,这种情况更容易发生。
2、当使用APC来实现定时器时,设置定时的线程不应该等待定时器的句柄。如果等待定时器的句柄的话,则唤起这个线程的原因是定时器被激活,而不是有实体加入到APC队列中。这时线程将不再处于监听状态,所以完全例程也不会被调用。在本例中,Sleep()被用于将线程置于监听状态。在定时器激活后,如果有实体被加入到此线程的APC队列中时,Sleep()就会唤醒此线程。
【示例代码】
#define _WIN32_WINNT 0x0500 #include <windows.h> #include <stdio.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; printf( "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; TCHAR szError[255]; MyData.szText = "This is my data."; MyData.dwValue = 100; if ( hTimer = CreateWaitableTimer( NULL, // Default security attributes FALSE, // Create auto-reset timer "MyTimer" ) ) // Name of waitable timer { __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 { wsprintf( szError, "SetWaitableTimer failed with Error \ %d.", GetLastError() ); MessageBox( NULL, szError, "Error", MB_ICONEXCLAMATION ); } } __finally { CloseHandle( hTimer ); } } else { wsprintf( szError, "CreateWaitableTimer failed with Error %d.", GetLastError() ); MessageBox( NULL, szError, "Error", MB_ICONEXCLAMATION ); } }
////////////////////////
waitable timer顾名思义,就是隔一段时间被signaled的一种内核对象。waitable timer跟event对象一样可以在创建的时候指定reset方式,如果是manual-reset,那么当waitable timer对象被signaled时,所有等待这个对象的wait函数都会返回。如果是auto-reset那么就只有一个wait函数会返回。
创建完waitable timer对象后,必须通过SetWaitableTimer函数对它进行时间上的设置。时间格式是个问题,看下面代码
// Declare our local variables. HANDLE hTimer; SYSTEMTIME st; FILETIME ftLocal, ftUTC; LARGE_INTEGER liUTC;
// Create an auto-reset timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// First signaling is at January 1, 2002, at 1:00 P.M. (local time). st.wYear = 2002; // Year st.wMonth = 1; // January st.wDayOfWeek = 0; // Ignored st.wDay = 1; // The first of the month st.wHour = 13; // 1PM st.wMinute = 0; // 0 minutes into the hour st.wSecond = 0; // 0 seconds into the minute st.wMilliseconds = 0; // 0 milliseconds into the second
SystemTimeToFileTime(&st, &ftLocal);
// Convert local time to UTC time. LocalFileTimeToFileTime(&ftLocal, &ftUTC); // Convert FILETIME to LARGE_INTEGER because of different alignment. liUTC.LowPart = ftUTC.dwLowDateTime; liUTC.HighPart = ftUTC.dwHighDateTime;
// Set the timer. SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);
上面的代码查下MSDN应该很容易理解,这里要说的是CPU对齐的问题。FILETIME结构必须位于32位边界,而LARGE_INTEGER必须位于64位边界,所以不能将FILETIME直接传给SetWaitableTimer。
SetWaitableTimer也可以使用时间的绝对值,或者使用相对时间值。不过这时的值必须是负的。看下面代码:
// Declare our local variables. HANDLE hTimer; LARGE_INTEGER li;
// Create an auto-reset timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// Set the timer to go off 5 seconds after calling SetWaitableTimer. // Timer unit is 100-nanoseconds. const int nTimerUnitsPerSecond = 10000000;
// Negate the time so that SetWaitableTimer knows we // want relative time instead of absolute time. // This indicate that the timer will be signaled 5 seconds after the call to SetWaitableTimer li.QuadPart = -(5 * nTimerUnitsPerSecond); // Set the timer. SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);
清除waitable timer对象需要用到CancelWaitableTimer函数。
特别提出的是waitable timer这节引出了一个新概念:APC(asynchronous procedure call)。按照我的理解,APC应该是线程特有的一个队列,里面装的是函数地址。如果一个函数地址被装入APC,如果这时线程处于待命的等待状态(alertable wait),那么这个线程就会被唤醒去调用APC里的函数;否则,APC里的函数地址就会被忽略掉。这里的这个线程指的是调用SetWaitableTimer的线程。下面的代码能说明问题
VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue) {
FILETIME ftUTC, ftLocal; SYSTEMTIME st; TCHAR szBuf[256];
// Put the time in a FILETIME structure. ftUTC.dwLowDateTime = dwTimerLowValue; ftUTC.dwHighDateTime = dwTimerHighValue;
// Convert the UTC time to the user's local time. FileTimeToLocalFileTime(&ftUTC, &ftLocal);
// Convert the FILETIME to the SYSTEMTIME structure // required by GetDateFormat and GetTimeFormat. FileTimeToSystemTime(&ftLocal, &st);
// Construct a string with the // date/time that the timer went off. GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR)); _tcscat(szBuf, _ _TEXT(" ")); GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, 0), sizeof(szBuf) / sizeof(TCHAR) - _tcslen(szBuf));
// Show the time to the user. MessageBox(NULL, szBuf, "Timer went off at...", MB_OK); }
void SomeFunc() { // Create a timer. (It doesn't matter whether it's manual-reset // or auto-reset.) HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
// Set timer to go off in 5 seconds. LARGE_INTEGER li = { 0 }; SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);
// Wait in an alertable state for the timer to go off. SleepEx(INFINITE, TRUE);
CloseHandle(hTimer); }
如果指定了APC,那么就不要等待这个waitable timer对象了,因为APC队列会唤醒线程的,不需要wait函数。
//////////////////////////////////////////////////////////////////////////////////////////
等待定时器(waitable timer)是在某个时间或按规定的时间间隔通知自己的内核对象。可以把它理解为一个定时发送信号的东西。
要创建一个等待定时器内核对象,可以调用函数CreateWaitableTimer。可以为该函数赋予不同的参数来指定一个定时器内核对象的属性。
HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName);
该函数第一个参数是安全属性结构指针。第三个参数是要创建的定时器内核对象名称。第二个参数指明了该定时器内核对象是人工重置(TRUE)的还是自动重置(FALSE)的。该函数成功,返回句柄,失败则返回NULL。
当一个人工重置的定时器内核对象收到通知时,所有等待在该内核对象上的线程都可以被唤醒,进入就绪状态。一个自动重置的定时器内核对象收到通知时,只有一个等待在该内核对象上的线程可以被调度。
当然,也可以打开一个特定名字的定时器内核对象,呼叫OpenWaitableTimer函数:
HANDLE OpenWaitableTimer( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);
等待定时器内核对象创建的时候的状态总是“未通知状态”。你可以呼叫SetWaitableTimer函数来设定等待定时器内核对象何时获得通知。
BOOL SetWaitableTimer( HANDLE hTimer, //等待定时器句柄 const LARGE_INTEGER *pDueTime, //第一次通知的时刻(负数表示相对值) LONG lPeriod, //以后通知的时间间隔(毫秒) PTIMERAPCROUTINE pfnCompletionRoutine, //APC异步函数地址 PVOID pvArgToCompletionRoutine, //APC异步函数参数 BOOL bResume); //是否让计算机摆脱暂停状态
该函数的第1个参数hTimer是一个等待定时器内核对象的句柄。
第2个参数pDutTime和第3个参数lPeriod要联合使用,pDutTime是一个LAGRE_INTEGER结构指针,指明了第一次通知的时间,时间格式是UTC(标准时间),是一个绝对值,如果要设置一个相对值,即让等待定时器在调用SetWaitableTimer函数之后多少时间发出第一次通知,只要传递一个负数给该参数即可,但是该数值必须是100ns的倍数,即单位是100ns,下面会举例说明。
第3个参数指明了以后通知的时间间隔,以毫秒为单位,该参数为0时,表示只有第一次的通知,以后没有通知。
第4和第5这两个参数与APC(异步过程调用)有关,这里不讨论。
最后一个参数bResume支持计算机暂停和恢复,一般传递FALSE。当它为TRUE的时候,当定时器通知的时候,如果此时计算机处于暂停状态,它会使计算机脱离暂停状态,并唤醒等待在该等待定时器上的线程。如果它为FALSE,如果此时计算机处于暂停状态,那么当该定时器通知的时候,等待在该等待定时器上的线程会被唤醒,但是要等待计算机恢复运行之后才能得到CPU时间。
比如,下面代码使用等待定时器让它在2008年8月8日晚上8:00开始通知。然后每隔1天通知。
HANDLE hTimer; //等待定时器句柄 SYSTEMTIME st; //SYSTEMTIME结构,用来设置第1次通知的时间 FILETIME ftLocal, ftUTC; //FILETIME结构,用来接受STSTEMTIME结构的转换 LARGE_INTEGER liUTC; //LARGE_INTEGER结构,作为SetWaitableTimer的参数 // 创建一个匿名的默认安全性的人工重置的等待定时器内核对象,并保存句柄 hTimer = CreateWaitableTimer(NULL, FALSE, NULL); //设置第一次通知时间 st.wYear = 2008; // 年 st.wMonth = 8; // 月 st.wDayOfWeek = 0; // 一周中的某个星期 st.wDay = 8; // 日 st.wHour = 20; // 小时(下午8点) st.wMinute = 8; // 分 st.wSecond = 0; // 秒 st.wMilliseconds = 0; // 毫秒 //将SYSTIME结构转换为FILETIME结构 SystemTimeToFileTime(&st, &ftLocal); //将本地时间转换为标准时间(UTC),SetWaitableTimer函数接受一个标准时间 LocalFileTimeToFileTime(&ftLocal, &ftUTC); // 设置LARGE_INTEGER结构,因为该结构数据要作为SetWaitableTimer的参数 liUTC.LowPart = ftUTC.dwLowDateTime; liUTC.HighPart = ftUTC.dwHighDateTime; // 设置等待定时器内核对象(一天的毫秒数为24*60*60*1000) SetWaitableTimer(hTimer, &liUTC, 24 * 60 * 60 * 1000, NULL, NULL, FALSE);
下面的代码创建了一个等待定时器,当调用SetWaitableTimer函数之后2秒会第一次通知,然后每隔1秒通知一次:
HALDLE hTimer; LARGE_INTEGER li; hTimer = CreateWaitableTime(NULL, FALSE, NULL); const int nTimerUnitsPerSecond = 100000000 / 100; //每1s中有多少个100ns li.QuadPart = -(2 * nTimerUnitsPerSecond ); //负数,表示相对值2秒 SetWaitableTimer(hTimer, &li, 1000, NULL, NULL, FALSE);
当通过SetWaitTimer函数设置了一个等待定时器的属性之后,你可以通过CancelWaitableTimer函数来取消这些设置:
BOOL CancelWaitableTimer(HANDLE hTimer);
当你不再需要等待定时器的时候,通过调用CloseHanble函数关闭之。
等待定时器与APC(异步过程调用)项排队:
Windows允许在等待定时器的通知的时候,那些调用SetWaitTimer函数的线程的异步过程调用(APC)进行排队。
要使用这个特性,需要在线程调用SetWaitTimer函数的时候,设置第4个参数pfnCompletionRoutine和第5的参数pvArgToCompletionRoutine。这个异步过程需要如下形式:
VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue) { // 特定的任务 }
该函数名TimerAPCRoutine可以任意。该函数可以在等待定时器收到通知的时候,由调用SetWaitableTimer函数的线程来调用,但是该线程必须处于“待命等待”状态。也就是说你的线程因为调用以下函数的而处于等待状态中:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx,MsgForMultipleObjectEx,SingleObjectAndWait。如果该线程没有因为调用这些函数而进入等待状态,那么系统不会给定时器APC排队。
下面讲一下详细的APC调用的过程:当你的等待定时器通知的时候,如果你的线程处于“待命等待”状态,那么系统就调用上面具有TimerAPCRoutine异步函数的格式的函数,该异步函数的第一个参数就是你传递给SetWaitableTimer函数的第5个参数pvArgToCompletionRoutine的值。其他两个参数用于指明定时器什么时候发出通知。
下面的代码指明了使用等待定时器的正确方法:
void SomeFunc() { // 创建一个等待定时器(人工重置) HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL); // 当调用SetWaitableTimer时候立刻通知等待定时器 LARGE_INTEGER li = { 0 }; SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE); // 线程进入“待命等待”状态,并无限期等待 SleepEx(INFINITE, TRUE); CloseHandle(hTimer); //关闭句柄 }
当所有的APC项都完成,即所有的异步函数都结束之后,等待的函数才会返回(比如SleepEx函数)。所以,必须确保等待定时器再次变为已通知之前,异步函数就完成了,这样,等待定时器的APC排队速度不会比它的处理速度慢。
注意,当使用APC机制的时候,线程不能应该等待“等待定时器的句柄”,也不应该以待命等待的方式等待“等待定时的句柄”,下面的方法是错误的:
HANDLE hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
SetWaitableTimer(hTimer, &li, 2000, TimerAPCRoutine, NULL, FALSE);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
这段代码让线程2次等待一个等待定时器,一个是等待该等待定时器的句柄,还有一个是“待命等待”。当定时器变为已通知状态的时候,该等待就成功了,然后线程被唤醒,导致线程摆脱了“待命等待”状态,APC函数不会被调用。
由于等待定时器的管理和重新设定是比较麻烦的,所以一般开发者很少使用这个机制,而是使用CreateThreadpoolTimer来创建线程池的定时器来处理问题。
等待定时器的APC机制也往往被I/O完成端口所替代。
最后,把“等待定时器”和“用户界面定时器”做一下比较。
用户界面定时器是通过SetTimer函数设置的,定时器一般发送WM_TIMER消息给调用SetTimer函数的线程和窗口,因此只能有一个线程收到通知。而“人工重置”的等待定时器可以让多个线程同时收到通知。
运用等待定时器,可以让你的线程到了规定的时间就收到通知。而用户界面定时器,发送的WM_TIMER消息属于最低优先级的消息,当线程队列中没有其他消息的时候才会检索该消息,因此可能会有一点延迟。
另外,WM_TIMER消息的定时精度比较低,没有等待定时器那么高。