线程通信机制之条件变量

线程通信机制之条件变量

——每周杂谈 第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 条件变量相关函数及描述

条件变量相关函数

说明

InitializeConditionVariable

初始化一个条件变量

SleepConditionVariableCS

以原子操作的方式同时执行以下两个操作:在指定条件变量上等待、释放特定的critical section。

SleepConditionVariableSRW

以原子操作的方式同时执行以下两个操作:在指定条件变量上等待并释放某个SRW锁。

WakeAllConditionVariable

唤醒所有在同一个条件变量上等待的多个线程。

WakeConditionVariable

唤醒在指定条件变量上等待的单个线程。

 

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)

 

注:版权所有,请勿用于商业用途,转载请注明原文地址。本人保留所有权利。

posted @ 2012-06-03 10:06  Tocy  阅读(1446)  评论(0编辑  收藏  举报