在并发编程的世界里,生产者-消费者问题就像是一个经典的黑森林蛋糕——看似简单,但稍有不慎就会让整个系统陷入混乱。想象一下,你正在开发一个实时数据处理系统,数据源源不断地从网络接口涌入(生产者),而分析模块则贪婪地消耗这些数据(消费者)。如何确保两者和谐共处,既不丢失数据,又不重复处理?这就是我们今天要探讨的核心问题。
生产者-消费者模型本质上是一个多线程协作问题,其中生产者线程生成数据放入共享缓冲区,而消费者线程从缓冲区取出数据进行处理。这个模型在现实中的应用无处不在:从消息队列系统到GUI事件处理,从日志记录器到视频流处理。
假设我们有一个简单的共享队列作为缓冲区:
cpp复制std::queue<int> buffer;
const int MAX_SIZE = 10;
如果没有同步机制,两个生产者可能同时检查队列未满,然后都尝试插入数据,导致数据覆盖或队列溢出。同样,消费者可能读取到无效或重复的数据。
在Windows平台下,我们可以使用两种同步原语:
cpp复制CRITICAL_SECTION csBuffer; // 保护buffer的访问
HANDLE hEventNotEmpty; // 缓冲区非空事件
HANDLE hEventNotFull; // 缓冲区未满事件
在程序开始时,我们需要初始化所有同步对象:
cpp复制void Initialize() {
InitializeCriticalSection(&csBuffer);
// 自动重置事件,初始状态为无信号
hEventNotEmpty = CreateEvent(NULL, FALSE, FALSE, NULL);
hEventNotFull = CreateEvent(NULL, FALSE, TRUE, NULL);
}
这里有两个关键设计决策:
生产者线程的核心逻辑是:
cpp复制DWORD WINAPI ProducerThread(LPVOID lpParam) {
while (true) {
WaitForSingleObject(hEventNotFull, INFINITE);
EnterCriticalSection(&csBuffer);
if (buffer.size() < MAX_SIZE) {
int data = GenerateData();
buffer.push(data);
std::cout << "Produced: " << data << std::endl;
// 如果插入后缓冲区刚好有一个元素,通知消费者
if (buffer.size() == 1) {
SetEvent(hEventNotEmpty);
}
}
LeaveCriticalSection(&csBuffer);
}
return 0;
}
消费者线程的对称逻辑:
cpp复制DWORD WINAPI ConsumerThread(LPVOID lpParam) {
while (true) {
WaitForSingleObject(hEventNotEmpty, INFINITE);
EnterCriticalSection(&csBuffer);
if (!buffer.empty()) {
int data = buffer.front();
buffer.pop();
std::cout << "Consumed: " << data << std::endl;
// 如果取出后缓冲区刚好有空位,通知生产者
if (buffer.size() == MAX_SIZE - 1) {
SetEvent(hEventNotFull);
}
}
LeaveCriticalSection(&csBuffer);
}
return 0;
}
我们当前使用的是自动重置事件(bManualReset=FALSE),它具有以下特点:
如果我们将事件创建为手动重置(bManualReset=TRUE):
考虑一个变种的生产者-消费者模型,其中多个消费者需要同时处理相同的数据(如发布-订阅模式),这时手动重置事件可能更合适:
cpp复制HANDLE hNewDataEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 生产者
void Produce() {
// ... 生产数据 ...
SetEvent(hNewDataEvent); // 通知所有消费者
}
// 消费者
void Consume() {
WaitForSingleObject(hNewDataEvent, INFINITE);
// ... 消费数据 ...
// 不需要ResetEvent,由生产者控制
}
双重检查锁定:在进入临界区前先做一次无锁检查
cpp复制if (buffer.empty()) {
WaitForSingleObject(hEventNotEmpty, INFINITE);
}
批量处理:减少锁的获取/释放次数
cpp复制EnterCriticalSection(&csBuffer);
for (int i = 0; i < batchSize && !buffer.empty(); ++i) {
Process(buffer.front());
buffer.pop();
}
LeaveCriticalSection(&csBuffer);
等待超时:避免死锁
cpp复制if (WAIT_TIMEOUT == WaitForSingleObject(hEventNotFull, 100)) {
// 处理超时逻辑
}
临界区内等待事件:这会导致死锁
cpp复制EnterCriticalSection(&csBuffer);
while (buffer.empty()) { // 错误!
WaitForSingleObject(hEventNotEmpty, INFINITE);
}
忘记设置事件:线程会永久阻塞
cpp复制buffer.push(data);
// 忘记调用 SetEvent(hEventNotEmpty);
事件与条件变量的混淆:事件没有记忆功能
cpp复制if (!buffer.empty()) { // 必须检查,事件只表示"发生过"
// 处理数据
}
考虑一个多线程日志系统,其中:
cpp复制class Logger {
std::queue<std::string> logQueue;
CRITICAL_SECTION csLog;
HANDLE hNewLogEvent;
public:
Logger() {
InitializeCriticalSection(&csLog);
hNewLogEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
}
void Log(const std::string& message) {
EnterCriticalSection(&csLog);
logQueue.push(message);
SetEvent(hNewLogEvent);
LeaveCriticalSection(&csLog);
}
void RunWriter() {
while (true) {
WaitForSingleObject(hNewLogEvent, INFINITE);
EnterCriticalSection(&csLog);
while (!logQueue.empty()) {
WriteToFile(logQueue.front());
logQueue.pop();
}
LeaveCriticalSection(&csLog);
}
}
};
另一个典型应用是性能监控系统,其中:
cpp复制struct PerfData {
time_t timestamp;
double cpuUsage;
double memoryUsage;
};
std::vector<PerfData> dataBuffer;
HANDLE hDataReadyEvent;
// 采样线程
void SamplingThread() {
while (running) {
PerfData data = CollectPerfData();
EnterCriticalSection(&csBuffer);
dataBuffer.push_back(data);
if (dataBuffer.size() >= BATCH_SIZE) {
SetEvent(hDataReadyEvent);
}
LeaveCriticalSection(&csBuffer);
Sleep(SAMPLING_INTERVAL);
}
}
// 分析线程
void AnalysisThread() {
while (running) {
WaitForSingleObject(hDataReadyEvent, INFINITE);
EnterCriticalSection(&csBuffer);
ProcessBatch(dataBuffer);
dataBuffer.clear();
LeaveCriticalSection(&csBuffer);
}
}