概述

创建可等待定时器是Windows内部线程同步的方式之一,本文简单讲述如何使用这一内核对象进行线程同步。

使用方法

创建对象:

//创建事件内核对象,默认未触发状态
	HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);

设置对象属性:

CreateWaitableTimer创建完成后内核对象处于未触发状态,需要使用API
	BOOL WINAPI SetWaitableTimer(
	__in          HANDLE hTimer,
	__in          const LARGE_INTEGER* pDueTime,
	__in          LONG lPeriod,
	__in          PTIMERAPCROUTINE pfnCompletionRoutine,
	__in          LPVOID lpArgToCompletionRoutine,
	__in          BOOL fResume
	);
	来设置计时器对象的一些属性,pDueTime是第一次触发时间(UTC时间),lPeriod表示在第一次触发后计时器应该
	以什么样的频率触发。


使用APC回调来处理定时器对象触发事件:(APC,即Asynchronous procedure call,异步程序调用

void CALLBACK TimerCallback( LPVOID lpArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue )
{//这里处理定时器触发
	int i = 0;
	SetEvent(g_hHelpEvent);
	//do something
}


void	WaitableTimerTest()
{
	//创建事件内核对象,默认未触发状态
	HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
	//创建事件,设置为未触发状态
	g_hHelpEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	SYSTEMTIME st;
	GetLocalTime(&st);
	//st.wSecond += 40;
	FILETIME ft;
	SystemTimeToFileTime(&st, &ft);
	//转换成UTC时间
	FILETIME ftUtc;
	LocalFileTimeToFileTime(&ft, &ftUtc);
	LARGE_INTEGER fTime;
	fTime.LowPart	= ftUtc.dwLowDateTime;
	fTime.HighPart	= ftUtc.dwHighDateTime;
	//当前时间的后一分钟触发,以后每10秒钟触发一次
	BOOL bRet = SetWaitableTimer(hTimer, &fTime, 10*1000, TimerCallback, (LPVOID)hTimer, FALSE);
	DWORD dwError = GetLastError();
	//将TimerCallback回调加入到系统APC队列后,只能通过SleepEx、WaitForSingleObjectEx、WaitForMultipObjectEx、
	//或者SignalObjectAndWait 进入等待状态APC回调函数才能被同一线程调用。
	while( WAIT_TIMEOUT == WaitForSingleObjectEx(g_hHelpEvent, 1000, FALSE) )
		SleepEx(1000*60, TRUE);
	switch( dwRet )
	{
	case WAIT_OBJECT_0://
		cout<<"定时器对象已经触发"<<endl;
		break;
	case WAIT_TIMEOUT:
		cout<<"等待超时,自动终止定时器对象"<<endl;
		CancelWaitableTimer(hTimer);
		break;
	case WAIT_FAILED:
		cout<<"WaitForSingleObject调用失败,系统错误码:%u"<<GetLastError()<<endl;
		break;
	}
	CloseHandle(hTimer);
	CloseHandle(g_hHelpEvent);
}


如何保证触发,关键在于:

//将TimerCallback回调加入到系统APC队列后,只能通过SleepEx、WaitForSingleObjectEx、WaitForMultipObjectEx、
//或者SignalObjectAndWait 进入等待状态APC回调函数才能被同一线程调用。也就是说只能使用这几个API阻塞当前线程,APC回调函数才会被改线程调用。
如果该参数为FALSE,函数不会返回直到超时已到。如果一个I/O回调函数出现,该函数也不会返回而且回调函数也不会执行。如果一个APC函数插入线程,该函数不会返回而且APC函数也不会执行。</span>
如果该参数为TRUE而且SleepEx与扩展I/O函数(ReadFileEx or WriteFileEx)是在同一个线程,函数就会立即返回当线程休眠超时或I/O回调函数出现。如果I/O回调函数出现,那么I/O回调函数会被调用。如果APC被插入线程,该函数不论当前线程是否超时都会立即执行,而且APC函数也会被调用。


Windows内核编程中说到的一个误区:

BOOL bRet = SetWaitableTimer(hTimer, &fTime, 10*1000, TimerCallback, (LPVOID)hTimer, FALSE);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
不应该这样来做,因为WaitForSingleObjectEx实际上会等待两次,一次是可提醒,另一次是内核对象句柄。当计时器触发时,等待成功,线程被唤醒,使得线程退出可提醒状态,APC函数不会被调用。


可等待定时器与Windows定时器消息的区别:

1、定时器消息需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源;可等待定时器是内核对象,不仅可以在多个线程间共享,而且具备安全性。

2、定时器消息只能针对一个线程,消息循环是以线程为单位的;可等待定时器则能改变多个线程的可调度状态。

3、WIM_TIMER消息总是优先级最低的,只有在线程的消息队列中没有其他消息了才会被处理。可等待计时器与其他内核对象没有什么不同,如果计时器触发而且线程正在处于等待状态,那么系统将唤醒线程。

最后:

需要注意的是,APC回调必须尽快处理完,以免发生一个回调还未处理完成另一个回调又来到了。


Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐