NOTE0
在以下两种基本情况下,线程之间需要相互通信:
- 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性;
- 一个线程需要通知其它线程某项任务已经完成
1.原子访问:Interlocked系列函数
http://hi.baidu.com/microsoftxiao/blog/item/a6411546296bc90c6a63e561.html该文章不错。
所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。
我们需要有一种方法能够保证对一个值的递增操作时原子操作——也就是说,不会被打断。Interlocked系列函数提供了我们需要的解决方案。
LONG InterlockedExchangeAdd(
PLONG volatile plAddend,
LONG lIncrement);
LONGLONG InterlockedExchangeAdd64(
PLONGLONG volatile pllAddend,
LONGLONG llIncrement);
只要调用这个函数,传一个长整形变量的地址和另一个增量值,函数就会保证递增操作是以原子方式进行的。
Interlocked 函数又是如何工作的呢?取决于代码运行的CPU平台。如果是x86系列CPU,那么Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其它CPU访问同一个内存地址。无论编译器如何生成代码,无论机器上装配了多少个CPU,这些函数都能够保证对值的修改时以原子方式进行的。
Interlocked函数执行得极快,调用一次Interlocked函数,通常只占用几个CPU周期(通常小于50),而且也不需要在用户模式和内核模式之间进行切换(这个切换通常需要占用1000个周期以上)。
当然,也可以用InterlockedExchangeAdd来做减法——只要在第二个参数中传入一个负值就行了。
下面是其它三个Interlocked函数:
LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue);
LONGLONG InterlockedExchange64(
PLONGLONG volatile plTarget,
LONGLONG lValue);
PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue);
实现自旋锁的时候,InterlockedExchange及其有用:
// Global variable indicating whether a shared resource is in use or not
BOOL g_fResourceInUse = FALSE; ...
void Func1() {
// Wait to access the resource.
while (InterlockedExchange (&g_fResourceInUse, TRUE) == TRUE)
Sleep(0);
// Access the resource.
...
// We no longer need to access the resource. InterlockedExchange(&g_fResourceInUse, FALSE);
}
在单CPU的机器上应避免使用旋转锁 。
PVOID InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lComparand);
PVOID InterlockedCompareExchangePointer(
PVOID* ppvDestination,
PVOID pvExchange,
PVOID pvComparand);
该函数对当前值( plDestination 参数指向的值)与 lComparand 参数中传递的值进行比较。如果两个值相同,那么* plDestination 改为 lExchange 参数的值。如果* plDestination 中的值与 lExchange 的值不匹配, * plDestination 保持不变。该函数返回* plDestination 中的原始值。记住,所有这些操作都是作为一个原子执行单位来进行的。
2.高级线程同步
如果我们只需要以原子方式修改一个值,那么Interlocked系列函数非常好用,我们当然应该优先使用它们。为了能够以“原子”方式访问复杂的数据结构,我们必须超越Interlocked系列函数。
我们既不应该使用旋转锁,也不应该进行轮循,因为浪费CPU时间是件很糟糕的事情。而应该调用函数把线程切换到等待状态,直到线程想要访问的资源可供使用为止。
volatile关键字:
volatile限定符告诉编译器这个变量可能被应用程序之外的其它东西修改,比如操作系统、硬件或者一个并发执行的线程。确切地说,volatile限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。给一个结构加volatile限定符等于给结构中所有的成员都加volatile限定符,这样可以确保任何一个成员始终都是从内存中读取的。
3.关键段
关键段 (critical section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。即,代码知道除了当前线程之外,没有任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其它线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其它线程的。
一般情况下,我们会将CRITICAL_SECTION结构作为全局变量来分配,这样进程中的所有线程就能够非常方便地通过变量名来访问这些结构。在使用 CRIICAL_SECTION的时候,只有两个必要条件:第一条件是所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址(我们可以通过自己喜欢的任何方式来把这个地址传给各个线程)。第二个条件是在任何线程试图访问被保护的资源之前,必须对 CRITICAL_SECTION结构的内部成员进行初始化。
下面这个函数用来对结构进行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
当知道线程不再需要访问共享资源的时候,我们应该调用下面的函数来清理CRITICAL_SECTION结构:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
然后我们在以下两个函数之间访问共享资源:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
。。。共享资源的访问。。。
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
EnterCriticalSection会执行下面的测试:
•如果没有线程正在访问该资源, EnterCriticalSection 便更新成员变量,以表示调用线程已被赋予访问权并立即返回,使该线程能够继续运行(访问该资源)。
•如果成员变量表明调用线程已经被赋予对资源的访问权,那么 EnterCriticalSection 便更新这些变量,以指明调用线程多少次被赋予访问权并立即返回,使该线程能够继续运行。这种情况很少出现,并且只有当线程在一行中两次调用 EnterCriticalSection 而不影响对 LeaveCriticalSection 的调用时,才会出现这种情况。
•如果成员变量指明,一个线程(除了调用线程之外)已被赋予对资源的访问权,那么 EnterCriticalSection 将调用线程置于等待状态。这种情况是极好的,因为等待的线程不会浪费任何CPU时间。系统能够记住该线程想要访问该资源并且自动更新 CRITICAL_SECTION 的成员变量,一旦目前访问该资源的线程调用 LeaveCriticalSection 函数,该线程就处于可调度状态。
我们可以用下面的函数的函数来代替EnterCriticalSection:
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
TryEnterCriticalSection从来不会让调用线程进入等待状态。 它会通过返回值来表示调用线程是否获准访问资源。如果资源正在被其它线程访问,那么返回值为FALSE,其它为TRUE。如果返回TRUE,那么CRITICAL_SECTION的成员已经更新过了,以表示该线程正在访问资源。因此,每个返回TRUE的 TryEnterCriticalSection调用必须有一个对应的 LeaveCriticalSection 。
当不能用Interlocked函数解决同步问题的时候,我们应该试一试关键段。关键段的最大好处在于它们非常容易使用,而且它们在内部也使用了Interlocked函数,因此执行速度非常快。关键段的最大缺点在于它们无法用来在多个进程之间对线程进行同步。
当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。为了提高关键段的性能,Microsoft把旋转锁合并到了关键段中。因此,当调用 EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权。只有尝试失败的时候,线程才会切换到内核模式并进入等待状态。
为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段:
http://blog.csdn.net/yuntongsf/archive/2009/07/31/4396451.aspx
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
dwSpinCount是我们希望旋转锁循环的次数。 这个值可以是0~0x00FFFFFF之间的任何一个值。在单处理器的机器上调用这个函数,那么函数会忽略 dwSpinCount参数,因此次数总是0。因为如果一个线程正在循环,那么占用资源的线程将没有机会放弃对资源的访问权。
我们可以调用一下函数来改变关键段的旋转次数:
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
用来保护进程堆的关键段锁使用的旋转次数大约是4000,这可以作为我们的一个参考值。
4.Slim读/写锁
SRWLock的目的和关键段相同:对一个资源进行保护,不让其它线程访问它。但是,与关键段不同的是,SRWLock允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有的读取者线程在同一时刻访问共享资源应该是可行的,这是因为仅仅读取资源的值并不存在破坏数据的风险。只有当写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下,写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下,写入者线程应该独占对资源的访问权:任何其它线程,无论是读取者线程还是写入者线程,都不允许访问资源。这就是SRWLock提供的全部功能。
首先,我们需要分配一个SRWLOCK结构并用InitializeSRWLock函数对它进行初始化:
VOID InitializeSRWLock(PSRWLOCK SRWLock);
一旦SRWLock初始化完成之后,写入者线程就可以调用AcquireSRWLockExclusive,将SRWLOCK对象的地址作为参数传入,以尝试获得对被保护资源的独占访问权。
VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
完成对资源的更新之后,应该调用ReleaseSRWLockExclusice,并将SRWLOCK对象的地址作为参数传入,这样就解除了对资源的锁定。
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
对读取者线程来说,同样有两个步骤,单调用的是下面两个新的函数:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
不存在用来删除或销毁SRWLOCK的函数,系统会自动执行清理工作。
与关键段相比,SRWLock缺乏下面两个特性:
•不存在TryEnter(Shared/Exclusive)SRWLock 之类的函数:如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive) 会阻塞调用线程。
•不能递归地调用SRWLOCK。也就是说,一个线程不能为了多次写入资源而多次锁定资源,然后再多次调用ReleaseSRWLock* 来释放对资源的锁定。
总结一下,如果希望在应用程序中得到最佳性能,那么首先应该尝试不要共享数据,然后依次使用volatile读取,volatile写入,Interlocked API,SRWLock以及关键段。当且仅当所有这些都不能满足要求的时候,再使用内核对象。因为每次等待和释放内核对象都需要在用户模式和内核模式之间切换,这种切换的CPU开销非常大。
5.一些有用的窍门和技巧
•以原子方式操作一组对象时使用一个锁;
•同时访问多个逻辑资源时以完全相同的顺序来获得资源的锁;
•不要长时间占用锁;
条件变量
Condition variables —— 条件变量,是Windows Vista中新增加的一种处理线程同步问题的机制。它可以与“关键代码段(critical section)”或“读写锁(SRWLock)”相互配合使用,来实现线程的同步,特别是实现类似“生产者-消费者”问题的时候,十分有效。
如果当前没有“产品”可供“消费者线程”(读者线程)使用,那么该“消费者线程”要释放掉对应的读写锁或者关键代码段,然后等待直到有一个新的“产品”被“生产者线程”(写者线程)制造出来之后,方可继续运行。
如果一个用来存放“产品”的数据结构满了(比如数组),那么对应“生产者线程”需要释放有关的锁和关键代码段,同时要等待“消费线程者”消费完这些“产品”。
条件变量机制就是为了简化上述“生产者-消费者”问题而设计的一种线程同步机制。当一个线程需要以原子的方式释放加在资源上的锁,并且需要被阻塞运行直到一个条件被满足,你可以呼叫如下的函数:
PCONDITION_VARIABLE pConditionVariable,
PCRITICAL_SECTION pCriticalSection,
DWORD dwMilliseconds);
PCONDITION_VARIABLE pConditionVariable,
PSRWLOCK pSRWLock,
DWORD dwMilliseconds,
ULONG Flags);
正如函数名所预示的那样,第一个函数针对关键代码段,第二个函数针对读写锁。
第1个参数pContidionVariable参数指向一个初始化的条件变量,该条件变量指明了调用者(一个线程)的条件变量,参数类型是CONDITION_VARIABLE的指针。
第2个参数指明了一个“关键代码段”或“读写锁”,它们是用来保护共享资源的。
第3个参数dwMilliseconds指明了你的线程想要等待多长时间,你可以传递INFINITE,指明需要无限期等待下去。
第二个函数的第4个参数Flags指明当条件变量满足的时候,你需要对应的锁做如何的要求(即返回的时候设置锁,该锁应该是什么类型的):在“生产者线程”(写者线程)中你应该传递0给该参数指明该锁是一个“排他锁”,该锁被线程独占;在“消费者线程”(读者线程)中你应该传递CONDITION_VARIABLE_LOCKMODE_SHARED给该参数指明该锁是一个“共享锁”,该锁能以共享的方式为“消费者线程”服务。
在该参数调用的时候,第二个参数所指定的关键代码段或读写锁会被释放,使得对应的线程可以访问共享资源,从而去“生产”或“消费”;在该函数返回的时候,这个锁又会被设置。如果是SRWLock,该函数返回的时候根据Flags参数设置读写锁类型:排他锁或共享锁。关键代码段则会被自动设置,因为关键代码段总是“排他”的。
如果等待超时,该函数返回FALSE,否则返回TRUE。
一个线程当被SleepConditionVariableCS或SleepConditionVariableSRW阻塞之后,可以被另一个线程通过呼叫WakeConditionVariable或WakeAllConditionVariable函数唤醒。
PCONDITION_VARIABLE ConditionVariable); //条件变量指针
PCONDITION_VARIABLE ConditionVariable); //条件变量指针
当你呼叫WakeConditionVariable函数的时候,传递一个条件变量的指针给它,此时,在一个等待在同样条件变量的上的线程的SleepConditionVariable函数内部,条件变量收到信号,通知线程,该函数会返回,同时把对应的锁设定为所需要的类型。
当你呼叫WakeAllConditionVarialbe函数的时候,一个过多个等待在相同条件变量上的线程会被被唤醒。唤醒多个线程是可以的,但是你需要在调用SleepConditionVariable*函数的时候设定参数Flags:给“生产者线程”传递0;给“消费者线程”传递CONDITION_VARIABLE_LOCKMODE_SHARED。所以,有些时候,“消费者线程”全部会被唤醒。或者这样唤醒:生产者、消费者、生产者、消费者……如此循环。
本书还举了一个例子,这里就不多说了,我个人总结了下,运用条件变量应该遵循如下模式(自己是这么认为的,如果有误还大家请指出)
CONDITION_VARIABLE g_cvProduce; //生产条件变量
CONDITION_VARIABLE g_cvConsume; //消费条件变量
SRWLOCK g_srwLock; //读写锁
DWORD WINAPI Consumer(PVOID pvParam) //消费者线程函数
{
AcquireSRWLockShard(&g_srwLock); //请求共享锁(读锁)
SleepConditionVariableSRW(g_cvConsume, &g_srwLock, INFINITE, CONDITION_VARIABLE_LOCKMODE_SHARED); //等待条件变量,会被生产者线程唤醒
//消费
ReleaseSRWLockShared(&g_srwLock); //释放共享锁
WakeConditionVariable(&g_cvProduce); //唤醒一个生产者线程
}
DWORD WINAPI Producer(PVOID pvParam) //生产者线程函数
{
AcquireSRWLockExclusive(&g_srwLock); //要求一个排他锁(写锁)
//等待条件变量受信,会被消费者线程唤醒
SleepConditionVariableSRW(g_cvProduce, &g_srwLock, INFINITE, 0);
//生产
用户模式的线程同步机制效率高,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法。
但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力。这时,只能考虑使用内核模式。
Windows提供了许多内核对象来实现线程的同步。对于线程同步而言,这些内核对象有两个非常重要的状态:“已通知”状态,“未通知”状态(也有翻译为:受信状态,未受信状态)。Windows提供了几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/错误流、事件、等待定时器、信号量、互斥对象。
你可以通知一个内核对象,使之处于“已通知状态”,然后让其他等待在该内核对象上的线程继续执行。你可以使用Windows提供的API函数,等待函数来等待某一个或某些内核对象变为已通知状态。
你可以使用WaitForSingleObject函数来等待一个内核对象变为已通知状态:
HANDLE hObject, //指明一个内核对象的句柄
DWORD dwMilliseconds); //等待时间
该函数需要传递一个内核对象句柄,该句柄标识一个内核对象,如果该内核对象处于未通知状态,则该函数导致线程进入阻塞状态;如果该内核对象处于已通知状态,则该函数立即返回WAIT_OBJECT_0。第二个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED。可以通过下面的代码来判断:
switch (dw)
{
case WAIT_OBJECT_0:
// hProcess所代表的进程在5秒内结束
break;
case WAIT_TIMEOUT:
// 等待时间超过5秒
break;
case WAIT_FAILED:
// 函数调用失败,比如传递了一个无效的句柄
break;
}
还可以使用WaitForMulitpleObjects函数来等待多个内核对象变为已通知状态:
DWORD dwCount, //等待的内核对象个数
CONST HANDLE* phObjects, //一个存放被等待的内核对象句柄的数组
BOOL bWaitAll, //是否等到所有内核对象为已通知状态后才返回
DWORD dwMilliseconds); //等待时间
该函数的第一个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值。phObjects参数是一个存放等待的内核对象句柄的数组。bWaitAll参数如果为TRUE,则只有当等待的所有内核对象为已通知状态时函数才返回,如果为FALSE,则只要一个内核对象为已通知状态,则该函数返回。第四个参数和WaitForSingleObject中的dwMilliseconds参数类似。
该函数失败,返回WAIT_FAILED;如果超时,返回WAIT_TIMEOUT;如果bWaitAll参数为TRUE,函数成功则返回WAIT_OBJECT_0,如果bWaitAll为FALSE,函数成功则返回值指明是哪个内核对象收到通知。
可以如下使用该函数:
//三个进程句柄
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); //等待3个进程结束
switch (dw)
{
case WAIT_FAILED:
// 函数呼叫失败
break;
case WAIT_TIMEOUT:
// 超时
break;
case WAIT_OBJECT_0 + 0:
// h[0](hProcess1)所代表的进程结束
break;
case WAIT_OBJECT_0 + 1:
// h[1](hProcess2)所代表的进程结束
break;
case WAIT_OBJECT_0 + 2:
// h[2](hProcess3)所代表的进程结束
break;
}
你也可以同时通知一个内核对象,同时等待另一个内核对象,这两个操作以原子的方式进行:
HANDLE hObjectToSignal, //通知的内核对象
HANDLE hObjectToWaitOn, //等待的内核对象
DWORD dwMilliseconds, //等待的时间
BOOL bAlertable); //与IO完成端口有关的参数,暂不讨论
该函数在内部使得hObjectToSignal参数所指明的内核对象变成已通知状态,同时等待hObjectToWaitOn参数所代表的内核对象。dwMilliseconds参数的用法与WaitForSingleObject函数类似。
该函数返回如下:WAIT_OBJECT_0,WAIT_TIMEOUT,WAIT_FAILED,WAIT_IO_COMPLETION。
等你需要通知一个互斥内核对象并等待一个事件内核对象的时候,可以这么写:
WaitForSingleObject(hEvent, INFINITE);
可是,这样的代码不是以原子的方式来操纵这两个内核对象。因此,可以更改如下:
本书首先介绍了一个重要的概念“成功的副作用”,这里笔者作一下简述。
当调用WaitForSingleObject和WaitForMultipleObject函数成功之后,该函数在返回成功的时候,系统可能会自动更改所等待的内核对象的状态,即将其从“已通知状态”切换为“未通知状态”。
当一个内核对象的状态被更改,称之为“成功等待的副作用”。比如,一个“自动重置”的事件内核对象,当调用等待函数成功返回的时候,该事件内核对象会由已通知状态转变为未通知状态。
比如此时有一个自动重置的事件内核对象hEvent,它处于未通知状态。线程T1、T2、T3内部调用“WaitForSingleObject(hEvent, INFINITE);”,这样当该事件内核对象变为“已通知”状态的话,T1线程“可能”被唤醒,但是其他的线程T2和T3呢?由于在T1线程内部WaitForSingleObject函数返回成功,又将hEvent事件内核对象设置为“未通知”状态,那么T2和T3就不可能被唤醒。
也就是说,“成功等待的副作用”会导致多个等待在同一个内核对象上的线程只能被唤醒一个。
好,下面我们来讨论“事件内核对象”。
在所有内核对象中,事件内核对象是最基本的一个内核对象。在事件内核对象内部,有以下几个比较重要的数据:
1、有一个“引用计数”:指明被打开的次数;
2、一个“布尔值”:指明该事件内核对象是自动重置的还是人工重置的;
3、另一个“布尔值”:指明该事件内核对象是“已通知状态”还是“未通知状态”。
事件内核对象可以通知一个事件已经完成。有两种不同的类型:自动重置和人工重置。当人工重置的事件内核对象得到通知的时候,所有等待在事件内核对象上的线程都变成可调度线程。当一个自动重置的事件内核对象得到通知的时候,等待在该事件内核对象上的线程只有一个能变成可调度状态。
要使用事件内核对象,首先调用CreateEvent函数来创建一个事件内核对象:
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
参数psa是一个SECURITY_ATTRIBUTES(安全属性)结构的指针,一般设置为默认安全,传递NULL。
bManualReset参数指定了该内核对象是人工重置(传递TRUE)的还是自动重置(传递FALSE)的。
bInitialState参数指定了该内核对象起始状态是已通知(传递TRUE)还是未通知状态(FALSE)。
pszName参数为要创建的事件内核对象起一个名字,如果传递NULL,则创建一个“匿名”的事件内核对象。如果不传递NULL,且系统中已经存在该名字的事件内核对象,则不创建新的事件内核对象而是打开这个已经存在的,返回它的句柄。
该函数如果成功,返回事件内核对象的句柄,这样就可以操纵它了。如果失败,返回NULL。
Windows Vista提供了另一个函数来创建事件内核对象:
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName,
DWORD dwFlags,
DWORD dwDesiredAccess);
该函数的psa和pszName参数的意义和函数CreateEvent相同。
参数dwFlags可以有以下数据的“位或组合”:
WinBase.h中定义的位组合数据 |
描述 |
---|---|
CREATE_EVENT_INITIAL_SET (0x00000002) |
如果设置了该数据,则表明事件内核对象的起始状态为已通知状态;否则起始状态为未通知状态。 |
CREATE_EVENT_MANUAL_RESET (0x00000001) |
如果设置了该数据,则表明事件内核对象是人工重置的;否则为自动重置的。 |
参数dwDesiredAccess可以让你对该事件内核对象的访问加一些限制,本书没有细说,查MSDN就可以了吧。
可以打开一个“命名”的事件内核对象:
DWORD dwDesiredAccess,
BOOL bInherit,
PCTSTR pszName);
第一个参数指明的访问的限制,第二个参数表示该事件内核对象的句柄能够被子进程继承,第三个参数指明了该事件内核对象的名字。该函数成功返回事件内核对象的句柄,失败返回NULL。
当不需要使用这些句柄时,需要调用CloseHandle函数来递减内核对象的引用计数,使得该内核对象可以被及时清除。
当一个事件内核对象被创建之后,你可以直接控制它的状态。你可以通知它,使得它从未通知状态转变为已通知状态:
也可以重新设置它,使它从已通知状态变为未通知状态:
一个自动重置的事件内核对象,如果等待成功,由于“成功等待的副作用”机制会将该事件内核对象由已通知状态变为未通知状态,这个时候就没有必要调用ResetEvent函数了。
如果是一个人工重置的事件内核对象,等待成功之后,并不会被设置为未通知状态,而是要程序员调用ResetRvent函数来使之转变为未通知状态。
还有要注意的就是,一个“自动重置”的事件内核对象收到通知,转变为已通知状态的时候,最多只能唤醒“一个”等待在它上的线程。一个“人工重置”的事件内核对象收到通知,转变为已通知状态的时候,能够唤醒“所有”等待在它上的线程。
等待定时器(waitable timer)是在某个时间或按规定的时间间隔通知自己的内核对象。可以把它理解为一个定时发送信号的东西。
要创建一个等待定时器内核对象,可以调用函数CreateWaitableTimer。可以为该函数赋予不同的参数来指定一个定时器内核对象的属性。
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);
该函数第一个参数是安全属性结构指针。第三个参数是要创建的定时器内核对象名称。第二个参数指明了该定时器内核对象是人工重置(TRUE)的还是自动重置(FALSE)的。该函数成功,返回句柄,失败则返回NULL。
当一个人工重置的定时器内核对象收到通知时,所有等待在该内核对象上的线程都可以被唤醒,进入就绪状态。一个自动重置的定时器内核对象收到通知时,只有一个等待在该内核对象上的线程可以被调度。
当然,也可以打开一个特定名字的定时器内核对象,呼叫OpenWaitableTimer函数:
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
等待定时器内核对象创建的时候的状态总是“未通知状态”。你可以呼叫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天通知。
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秒通知一次:
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函数来取消这些设置:
当你不再需要等待定时器的时候,通过调用CloseHanble函数关闭之。
等待定时器与APC(异步过程调用)项排队:
Windows允许在等待定时器的通知的时候,那些调用SetWaitTimer函数的线程的异步过程调用(APC)进行排队。
要使用这个特性,需要在线程调用SetWaitTimer函数的时候,设置第4个参数pfnCompletionRoutine和第5的参数pvArgToCompletionRoutine。这个异步过程需要如下形式:
DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
// 特定的任务
}
该函数名TimerAPCRoutine可以任意。该函数可以在等待定时器收到通知的时候,由调用SetWaitableTimer函数的线程来调用,但是该线程必须处于“待命等待”状态。也就是说你的线程因为调用以下函数的而处于等待状态中:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx,MsgForMultipleObjectEx,SingleObjectAndWait。如果该线程没有因为调用这些函数而进入等待状态,那么系统不会给定时器APC排队。
下面讲一下详细的APC调用的过程:当你的等待定时器通知的时候,如果你的线程处于“待命等待”状态,那么系统就调用上面具有TimerAPCRoutine异步函数的格式的函数,该异步函数的第一个参数就是你传递给SetWaitableTimer函数的第5个参数pvArgToCompletionRoutine的值。其他两个参数用于指明定时器什么时候发出通知。
下面的代码指明了使用等待定时器的正确方法:
{
// 创建一个等待定时器(人工重置)
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消息的定时精度比较低,没有等待定时器那么高。