线程通信机制之条件变量
线程通信机制之条件变量
——每周杂谈 第005篇
作者:Tocy 时间:2012-05-28
关键词:条件变量,ITC,线程同步
作为同步原语之一,条件变量(condition variables)可以使线程在某个特定条件处等待。条件变量是用户模式下的对象,因此不能用于进程之间的通信。
条件变量可以使线程自动释放已经获得的锁,同时进入休眠状态(不占用CPU)。条件变量可以和Critical Section(临界区、关键区)、SRW锁(Slim reader/writer lock,slim读写锁,笔者通常使用轻量级读写锁)同时使用。条件变量支持唤醒一个或者多个等待线程。线程被唤醒之后,会重新获得锁(该锁为当前线程进入休眠状态时释放的锁)。
需要说明的是Windows Server 2003和Windows XP不支持条件变量。也就是说条件变量使用的最低要求是Windows Vista操作系统。
表格 1 条件变量相关函数及描述
条件变量相关函数 |
说明 |
初始化一个条件变量 |
|
以原子操作的方式同时执行以下两个操作:在指定条件变量上等待、释放特定的critical section。 |
|
以原子操作的方式同时执行以下两个操作:在指定条件变量上等待并释放某个SRW锁。 |
|
唤醒所有在同一个条件变量上等待的多个线程。 |
|
唤醒在指定条件变量上等待的单个线程。 |
MSDN中给出了一个使用条件变量的例子——实现生产者/消费者模型。使用队列表示有界循环缓冲区,并使用critical section保护该队列。示例代码中有两个条件变量:一个是生产者使用的(BufferNotFull),另一个是消费者使用的(BufferNotEmpty)。消费者线程调用SleepConditionVariableCS函数来等待产品添加到队列中,使用WakeConditionVariable 函数来通知生产者队列可用。生产者线程调用SleepConditionVariableCS函数来等待产品被消费者从队列中取走,使用WakeConditionVariable 函数来通知消费者队列中有产品。
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFFER_SIZE 10
#define PRODUCER_SLEEP_TIME_MS 500
#define CONSUMER_SLEEP_TIME_MS 2000
LONG Buffer[BUFFER_SIZE];
LONG LastItemProduced;
ULONG QueueSize;
ULONG QueueStartOffset;
ULONG TotalItemsProduced;
ULONG TotalItemsConsumed;
CONDITION_VARIABLE BufferNotEmpty;
CONDITION_VARIABLE BufferNotFull;
CRITICAL_SECTION BufferLock;
BOOL StopRequested;
DWORD WINAPI ProducerThreadProc (PVOID p)
{
ULONG ProducerId = (ULONG)(ULONG_PTR)p;
while (true)
{
// Produce a new item.
Sleep (rand() % PRODUCER_SLEEP_TIME_MS);
ULONG Item = InterlockedIncrement (&LastItemProduced);
EnterCriticalSection (&BufferLock);
while (QueueSize == BUFFER_SIZE && StopRequested == FALSE)
{
// Buffer is full - sleep so consumers can get items.
SleepConditionVariableCS (&BufferNotFull, &BufferLock, INFINITE);
}
if (StopRequested == TRUE)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Insert the item at the end of the queue and increment size.
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = Item;
QueueSize++;
TotalItemsProduced++;
printf ("Producer %u: item %2d, queue size %2u\r\n", ProducerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a consumer is waiting, wake it.
WakeConditionVariable (&BufferNotEmpty);
}
printf ("Producer %u exiting\r\n", ProducerId);
return 0;
}
DWORD WINAPI ConsumerThreadProc (PVOID p)
{
ULONG ConsumerId = (ULONG)(ULONG_PTR)p;
while (true)
{
EnterCriticalSection (&BufferLock);
while (QueueSize == 0 && StopRequested == FALSE)
{
// Buffer is empty - sleep so producers can create items.
SleepConditionVariableCS (&BufferNotEmpty, &BufferLock, INFINITE);
}
if (StopRequested == TRUE && QueueSize == 0)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Consume the first available item.
LONG Item = Buffer[QueueStartOffset];
QueueSize--;
QueueStartOffset++;
TotalItemsConsumed++;
if (QueueStartOffset == BUFFER_SIZE)
{
QueueStartOffset = 0;
}
printf ("Consumer %u: item %2d, queue size %2u\r\n",
ConsumerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a producer is waiting, wake it.
WakeConditionVariable (&BufferNotFull);
// Simulate processing of the item.
Sleep (rand() % CONSUMER_SLEEP_TIME_MS);
}
printf ("Consumer %u exiting\r\n", ConsumerId);
return 0;
}
int main ( void )
{
InitializeConditionVariable (&BufferNotEmpty);
InitializeConditionVariable (&BufferNotFull);
InitializeCriticalSection (&BufferLock);
DWORD id;
HANDLE hProducer1 = CreateThread (NULL, 0, ProducerThreadProc, (PVOID)1, 0, &id);
HANDLE hConsumer1 = CreateThread (NULL, 0, ConsumerThreadProc, (PVOID)1, 0, &id);
HANDLE hConsumer2 = CreateThread (NULL, 0, ConsumerThreadProc, (PVOID)2, 0, &id);
puts ("Press enter to stop...");
getchar();
EnterCriticalSection (&BufferLock);
StopRequested = TRUE;
LeaveCriticalSection (&BufferLock);
WakeAllConditionVariable (&BufferNotFull);
WakeAllConditionVariable (&BufferNotEmpty);
WaitForSingleObject (hProducer1, INFINITE);
WaitForSingleObject (hConsumer1, INFINITE);
WaitForSingleObject (hConsumer2, INFINITE);
printf ("TotalItemsProduced: %u, TotalItemsConsumed: %u\r\n",
TotalItemsProduced, TotalItemsConsumed);
return 0;
}
注:这段代码值得学习的地方,不止包括条件变量的使用,还有critical section的使用,循环队列的操作,多线程的程序设计,程序结束很优雅。唯一不足就是最后没有关闭创建线程句柄。
条件变量容易产生虚假唤醒(和显式唤醒完全不相关)和唤醒窃取现象(另一个线程在当前唤醒线程之前成功获得临界资源访问权并运行)。因此,在sleep操作返回之后你需要重新检查谓词(临界条件是否成立)。
可以在和条件变量相关的锁内部或者外部调用WakeConditionVariable / WakeAllConditionVariable 函数来唤醒其他线程。MSDN推荐先释放资源锁,然后唤醒其他线程以减少上下文切换的次数。
通常用法是针对同一个锁,使用多个条件变量。例如读写锁的的一个实现思路是使用同一个critical section,但是针对读和写使用不同的条件变量。
本文参考:http://msdn.microsoft.com/en-us/library/ms682052(v=vs.85)
注:版权所有,请勿用于商业用途,转载请注明原文地址。本人保留所有权利。
----------------------------------------------------------------------------------------------------------------------------
本文作者:Tocy e-mail: zyvj@qq.com
版权所有@2015-2020,请勿用于商业用途,转载请注明原文地址。本人保留所有权利。