第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

时间:2024-04-16 11:32:47

9.4 可等待的计时器内核对象——某个指定的时间或每隔一段时间触发一次

(1)创建可等待计时器:CreateWaitableTimer(使用时应把常量_WIN32_WINNT定义为0x0400

参数

描述

psa

安全属性(如使用计数、句柄继承等)

bManualReset

手动重置计时器还是自动重置计时器。

①当手动计时器被触发所有正在等待计时器的线程都变可为可调度。

②当自动计时器被触发时只有一个正在等待计数器的线程变为可调度

pszName

对象的名字

(2)也可以打开一个己经存在的可等待计时器:OpenWaitableTimer

(3)设置可等待计时器状态:SetWaitableTimer

参数

描述

HANDLE hTimer

要想触发的计时器

LARGE_INTEGER*  pDueTime

计时器第1次被触发的时间(应该为世界协调时UTC

说明:pDueTime为正数时是个绝对时间为负数时,表示一个相对时间,表示要在相对于调用该函数以后多少个(100ns)毫秒应第1次触发计时器。如5秒后,则应为

-5*10 000 000

LONG lPeriod

第一次触发后,每隔多少时触发一次(单位是微秒)。

如果希望计时器只触发一次,之后不再触后,该参数为0.

PTIMERAPCROUTINE pfnCR

要加入APC队列的回调函数

PVOID pvArgToCR

传给回调函数的额外参数

BOOL bResume

如果为TRUE,而且系统支持电源管理,那么在计时器触发的时候,系统会退出省电模式。如设为TRUE,但系统不支持省电模式,GetLastError就会返回ERROR_NOT_SUPPORTED 适用平台。一般设为FALSE

(4)取消计时器:CancelWaitableTimer,调用后计时器永远不会触发。

(5)计数器的用法

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

  ①利用CreateWaitableTimer创建计时器对象

  ②调用SetWaitableTimer来指定计时器首次触发及以后触发的时间间隔等。

  ③调用等待函数将调用线程挂起,直到计时器对象被触发。

  ④最后使用CloseHandle关闭计时器对象。

【SetWaitableTimer伪代码】——设置计时器在2015年8月18日14:00触发,以后每隔6小时触发一次

HANDLE hTimer;
SYSTEMTIME st = { };
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC; //创建一个自动的计时器对象
hTimer = CreateWaitableTimer(NULL, FALSE, NULL); //首先,设置时间为2015年8月18日,14:00(本地时间)
st.wYear = ;
st.wMonth = ;
st.wDay = ;
st.wHour = ; //PM格式的
st.wMinute = ;
st.wSecond = ;
st.wMilliseconds = ; SystemTimeToFileTime(&st, &ftLocal); //将本地时间转为UTC时间
LocalFileTimeToFileTime(&ftLocal, &ftUTC); //将FILETIME转为LARGE_INTEGER(因为对齐方式不同)
//FILETIME结构的地址必须是4的整数倍(32位边界),
//而LARG_INTEGER结构的地址必须是8的整数倍(64位边界)
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime; //设置计时器
//SetWaitableTimer传入的时间始终是UTC时间(这个时间必须是64位边界)
//在liUTC后,每隔6个小时触发一次
SetWaitableTimer(hTimer, &liUTC, * * * , NULL, NULL, FALSE);

9.4.1 线程APC队列及线程的可警告状态

(1)线程可警告状态

  ①通过一些方法让线程假“暂停”(注意此时线程仍被分配CPU时间片),转而去执行线程APC队列中的函数。线程的这种假“暂停”的状态,被称为“可警告”状态(或“可提醒”状态)。让线程进入可警告状态的方法可通过如调用SleepEx、Wait*Ex函数族。

  ②在线程进入可警告状态前,系统需要保存为线程函数分配的调用栈及各寄存器状态,然后再转向执行APC队列中的函数。

  ③此时对于线程函数来说确实是被暂停执行,进入“等待+可警告”状态只有当APC队列中的所有函数都被执行完毕后线程函数才会被唤醒来继续执行(实际上线程并未真正睡眠,只是被中断去执行APC函数,完毕又回来到该线程函数中来)

(2)线程APC队列(异步过程调用)

  ①线程APC队列其实就是为线程在线程函数之外,再安排一组函数去执行,本质上是利用线程实质是函数调用器的性质。

  ②默认情况下,创建线程时不会创建这个队列,但当调用了QueueUserAPC函数或其他可向APC队列添加实体的函数后,才会创建APC这个队列。

  ③可以用Wait函数族或SleepEx函数并传入bAlterable为TRUE,让线程进入一种假“暂停”的状态。这时系统调度器会转向调用线程APC队列中的函数

  ④需要注意的是有些函数也会使线程进入“等待状态”(没CPU时间片),但不是可警告状态。这时,线程不会转向执行APC队列。一般这些函数中没有bAlterable这个参数)

  ⑤不要在APC函数中调用让线程进入Alterable状态的API,这会引起递归,而导致线程栈溢出。

【ThreadAlterable程序】线程可警告示例程序

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h> VOID CALLBACK APCFunc(ULONG_PTR dwParam)
{
int i = dwParam;
_tprintf(_T("%d APC Function Run!\n"),i);
Sleep();
} int _tmain()
{
//为主线程添加APC函数
for (int i = ; i < ;i++){
QueueUserAPC(APCFunc, GetCurrentThread(), i);
} //SleepEx会让主线程进入一种假“暂停”状态。实际上,系统调度器只是暂停主线程函数本身,转而去执行线程中APC队列中的函数而己。
//但主线程实际上没有暂停,只是时间片给了,APC队列中的函数,而不是线程函数而己。这种状态叫“等待+可警告状态”。当该句注释后,
//因线程没有进入可警告状态,所以APC队列中的函数并不会被执行。
SleepEx(INFINITE, TRUE);
_tsystem(_T("PAUSE"));
return ;
}

【ThreadAPC程序】演示线程APC队列的执行

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h> DWORD WINAPI ThreadProc(PVOID pvParam)
{
HANDLE hEvent = (HANDLE)pvParam; _tprintf(_T("线程函数正在等待中...\n"));
Sleep();
//使该线程假挂起(仍然分配CPU时间片),只是这时
//不并执行该线程函数(给人造成线程暂停的假象),实际上这时
//该线程仍在执行,只是转向去执行队列APC中的回调函数罢了。
//该函数会在hEvent被触发,或APC队列执行完毕后返回
DWORD dw = WaitForSingleObjectEx(hEvent, INFINITE, TRUE); //可警告状态
switch (dw)
{
case WAIT_OBJECT_0:
_tprintf(_T("线程函数:事件触发!"));
break; case WAIT_IO_COMPLETION:
//如果线程至少处理了APC队列中的一项
_tprintf(_T("线程函数:APC队列中APC函数执行完,等待函数返回WAIT_IO_COMPLETION\n"));
break;
case WAIT_FAILED:
_tprintf(_T("调用等待函数失败:%u\n"),GetLastError());
break;
} _tprintf(_T("线程函数运行结束!"));
return ;
} VOID CALLBACK APCFunc(ULONG_PTR dwParam)
{
int i = dwParam;
_tprintf(_T("%d APC Func Run!\n"), i);
Sleep(); //该线程真正被挂起(不再分配CPU时间片)
} int _tmain()
{
_tsetlocale(LC_ALL, _T("chs")); //创建自动、未触发的事件对象(即每次只唤醒一个线程)
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //创建线程
HANDLE hThread = CreateThread(NULL, , ThreadProc, hEvent, , NULL); Sleep(); //主线程休眠,以便子线程进入可警告状态 //在子线程中加入APC队列函数
for (int i = ; i < ; i++){
QueueUserAPC(APCFunc, hThread, i);
} //等待子线程结束
WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread);
CloseHandle(hEvent);
_tsystem(_T("PAUSE"));
return ;
}

9.4.2 利用可等待计时器将APC函数添加到APC队列中

(1)计时器时间一到,会触发计时器对象。但也可以在时间到时把一个APC添加到队列中。

(2)计时器APC回调函数的原型

VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,DWORD dwTimerLowValue,DWORD dwTimerHighValue);

  ①函数名可随意,这里取名为TimerAPCRoutine。后面两个参数系统会传入计时器被触发时的时间(UTC时间)。

  ②可调用SleepEx、WaitForSingle\MultipleObjectEx、SignalObjectAndWait、MsgWaitForMultipleObjectEx等函数让线程进入可警告状态。当线程进入可警告状态时,会转去执行APC队列中的函数。换句话说就是,只有当线程被设为可警告状态,加入到APC队列中的函数才是可被回调的。

  ③当计时器被触发并且线程处于可警告状态时,系统会让线程调用回调函数。

(3)注意事项

  ①当计时器被触发时,会向APC队列添加一个回调函数(如MyAPC),并转向去执行该函数。但由于APC队列的特点,在该函数执行完后,系统会再去检查队列中剩下的其它APC函数。只有当队列中其他函数都执行完毕,这个MyAPC函数才会返回。因此,必须确保TimerAPCRoutine函数在计时器再次被触发之前结束,否则函数加入队列的速度会超过处理它们的速度。

  ②当线程调用Wait*或SleepEx函数而进入等待状态时,这些等待函数会在下列几种情况中返回:A、等待函数所等待的对象被触发(返回值为WAIT_OBJECT_x之类);B、APC队列中所有函数执行完毕(返回值为WAIT_IO_COMPLETION);C、等待超时(返回值为WAIT_TIMEOUT);D、在调用MsgWaitForMutipleObjectsEx时,一个消息进入到线程的消息队列时。因此,下列的调用是错误的:

HANDLE hTimer = CreateWaitableTimer(NULL,FALSE,NULL);

SetWaitableTimer(hTimer,…,TimerAPCRoutine,…);

//当计时器触发时,内核对象hTimer被设为有信号,Wait*函数会返回,线程被“唤醒”,使得线程退出可警告状态,因此,APC函数无法被调用!
WaitForSingleObjectEx(hTimer,INFINITE,TRUE); //不能在等待计时器的同时,再以可警告来等待。

9.4.3 计时器的剩余问题

  ①用户计时器是通用SetTimer来设置的,一般会发送WM_TIMER给调用SetTimer的线程(对于回调计时器来说)或者窗口过程(对基于窗口的计时器来说),因此只有一个线程可以收到通知。而“手动重置”的等待计时器可以让多个线程同时收到通知。

  ②等待计时器可以让线程到了规定的时间就收到通知。而用户计时器,发送的WM_TIMER消息属于最低优先级的消息,当线程队列中没有其他消息的时候才会检索该消息,因此可能会有一点延迟,甚至有的消息会被丢弃。

  ③WM_TIMER消息的定时精度比较低,没有等待计时器那么高。

  ④用户计时器需要使用大量的用户界面设施,从而消耗更多的资源。等待计时器是内核对象,不仅可以在多线程间共享,而且可以具备安全性。

【Timer(NonAPC)程序】可等待计时器(非APC)

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h> int CreateTimer1();
int CreateTimer2(); int _tmain()
{
_tsetlocale(LC_ALL, _T("chs")); _tprintf(_T("创建绝对时间的计时器对象\n"));
CreateTimer1(); _tprintf(_T("\n")); _tprintf(_T("创建相对时间的计时器对象\n"));
CreateTimer2(); _tsystem(_T("PAUSE"));
return ;
} //创建绝对时间的计时器
int CreateTimer1()
{
//声明局部变量
HANDLE hTimer;
SYSTEMTIME st = { };
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC; //创建计时器对象
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
if (!hTimer)
return -; _tprintf(_T("第一次触发时间为2015-8-19 08:30:00,并每隔3秒报时一次(循环3次)\n")); //设置开始时间
st.wYear = ;
st.wMonth = ;
st.wDay = ;
st.wHour = ;
st.wMinute = ;
st.wSecond = ;
st.wMilliseconds = ; //系统时间转换文件时间
SystemTimeToFileTime(&st, &ftLocal); //本地时间转换为UTC时间
LocalFileTimeToFileTime(&ftLocal, &ftUTC); //转换FILETIME为LLARGE_INTEGER,为了变量对齐边界
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime; //设置计时器
if (!SetWaitableTimer(hTimer,&liUTC,*,NULL,NULL,FALSE)){
CloseHandle(hTimer);
return -;
} //等待计时器触发
for (int i = ; i < ;i++){
if (WaitForSingleObject(hTimer,INFINITE)!=WAIT_OBJECT_0){
_tprintf(_T("计时器出错了\n"));
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
}
else
{
GetLocalTime(&st);
//3秒钏到达,获取系统时间
_tprintf(_T("3秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%d\n"),
i + , st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}
}
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
return ;
} //创建相对时间的计时器
int CreateTimer2()
{
//声明局部变量
HANDLE hTimer;
LARGE_INTEGER liDueTime;
SYSTEMTIME st = { }; //设置相对时间为5秒
liDueTime.QuadPart = -*;//单位(100ns),设置相对时间时,必须为负数 //创建计时器对象
hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
if (!hTimer)
return -; GetLocalTime(&st);
_tprintf(_T("创建5秒计时器,现在系统时间为:%d-%d-%d %d:%d:%d\n"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); //设置计时器
//调用SetWaitableTimer的时候,给第2个参数传递一个相对的时间在值,这个参数必须是
//负数,表示自调用函数后若干秒后,计时器触发。
if (!SetWaitableTimer(hTimer, &liDueTime, , NULL, NULL, FALSE)){
CloseHandle(hTimer);
return -;
} //等待计时器触发
if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0){
_tprintf(_T("计时器出错了\n"));
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
} else
{
GetLocalTime(&st);
//3秒钏到达,获取系统时间
_tprintf(_T("5秒到了,系统时间为:%d-%d-%d %d:%d:%d\n"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
return ;
}

【Timer(APC)程序】可等待计时器(APC)

第9章 用内核对象进行线程同步(2)_可等待计时器(WaitableTimer)

#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <time.h> VOID CALLBACK TimerAPCProc(LPVOID lpArgToCompletionRoutine,
DWORD dwTimerLowValue,DWORD dwTimerHighValue); int _tmain()
{
_tsetlocale(LC_ALL, _T("chs")); HANDLE hTimer;
SYSTEMTIME st; //创建一个可等待的计时器对象(手动或自动均可)
hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = - * ; //获得现在系统时间并输出
GetLocalTime(&st);
_tprintf(_T("创建5秒计时器,现在系统时间为:%d-%d-%d %d:%d:%d\n"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); //设置计时器5秒后触发
SetWaitableTimer(hTimer, &liDueTime, *, TimerAPCProc, NULL, TRUE); ////线程进入假“暂停”的可警告状态
//SleepEx(INFINITE, TRUE); //只等待一次触发,该函数就会返回 //等待计时器触发
for (int i = ; i < ; i++){
if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0){
_tprintf(_T("计时器出错了\n"));
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
} else
{
GetLocalTime(&st);
//3秒钏到达,获取系统时间
_tprintf(_T("5秒到了,第%d次触发。系统时间为:%d-%d-%d %d:%d:%d\n"),
i + , st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}
} //关闭可等待计时器对象
CancelWaitableTimer(hTimer);
CloseHandle(hTimer); _tsystem(_T("PAUSE"));
return ;
} //APC回调函数
VOID CALLBACK TimerAPCProc(LPVOID lpArgToCompletionRoutine,
DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
FILETIME ftUTC,ftLocal;
SYSTEMTIME st; ftUTC.dwLowDateTime = dwTimerLowValue;
ftUTC.dwHighDateTime = dwTimerHighValue; FileTimeToLocalFileTime(&ftUTC, &ftLocal);
FileTimeToSystemTime(&ftLocal, &st); _tprintf(_T("5秒到了,系统时间为:%d-%d-%d %d:%d:%d\n"),
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}