我想为2个线程实现一个消息队列。线程#1将弹出队列中的消息并对其进行处理。线程#2会将消息推送到队列中。
这是我的代码:
Thread #1 //Pop message and process
{
while(true)
{
Lock(mutex);
message = messageQueue.Pop();
Unlock(mutex);
if (message == NULL) //the queue is empty
{
//assume that the interruption occurs here (*)
WaitForSingleObject(hWakeUpEvent, INFINITE);
continue;
}
else
{
//process message
}
}
}
Thread #2 //push new message in queue and wake up thread #1
{
Lock(mutex);
messageQueue.Push(newMessage)
Unlock(mutex);
SetEvent(hWakeUpEvent);
}
问题是有些情况SetEvent(hWakeUpEvent)
将在WaitForSingleObject()
之前调用(注意(*)),这将是危险的。
答案 0 :(得分:3)
你的代码没问题!
SetEvent和WaitForSingleObject之间的时间没有实际问题:关键问题是事件上的WaitForSingleObject将检查事件的状态,并等待它被触发。如果事件已被触发,则会立即返回。 (在技术术语中,它是由级别触发的,而不是边缘触发的。)这意味着如果在调用WaitForSingleObject之前或期间调用SetEvent,则可以正常运行。 WaitForSingleObject将在任何一种情况下返回;立即或稍后调用SetEvent时。
(顺便说一句,我假设在这里使用自动重置事件。我想不出使用手动重置事件的好理由;你最终必须在WaitForSingleObject返回后立即调用ResetEvent;并且有如果忘记了这一点,你可能最终会等待你已经等待但忘记清除的事件。此外,在检查基础数据状态之前重置是很重要的,否则如果在数据之间调用SetEvent则如果处理了,则会调用Reset(),丢失该信息。坚持使用自动重置,并避免这一切。)
-
[编辑:我误读了OP的代码,因为在每个唤醒上执行单个'pop',而不是只等待空,所以下面的注释引用代码那个场景。 OP的代码实际上等同于下面的第二个建议修复。所以下面的文字实际上描述了一个有点常见的编码错误,其中事件被用作信号量,而不是OP的实际代码。]
但是这里有一个不同的问题[或者,如果每次等待只有一个pop ...],那就是Win32 Events对象只有两个状态:unsignaled和signaled,所以你只能使用它们跟踪二进制状态,但不计算。如果您已经发出信号的SetEvent和事件,它将保持信号状态,并且该额外的SetEvent调用的信息将丢失。
在这种情况下,可能发生的事情是:
有两种解决方法:经典的Comp.Sci方法是使用信号量而不是事件 - 信号量本质上是计算所有“Set”调用的事件;相反,你可以将事件视为最大数为1的信号量,忽略超出该信号的任何其他信号。
另一种方法是继续使用事件,但是当工作线程唤醒时,它只能假设队列中可能有一些项,并且应该先尝试处理它们。它返回等待 - 通常是将弹出项目的代码放在一个循环中,弹出项目并处理它们直到它为空。该事件现在用于不计数,而是用于表示“队列不再为空”。 (请注意,当您执行此操作时,您还可以获得这样的情况:在处理队列时,您还处理刚刚添加的项以及调用了SetEvent的项,以便当工作线程到达WaitForSingleObject时,线程会唤醒但是发现队列是空的,因为项目已经被处理;一开始看起来有点令人惊讶,但实际上很好。)
我认为这两者大致相同;这两者都有微小的优点和缺点,但它们都是正确的。 (我个人更喜欢事件方法,因为它从“工作或数据的数量”中分离出“需要完成的事情”或“更多数据可用”的概念。)
答案 1 :(得分:2)
“经典”方式(即肯定能正常工作)是使用信号量,(参见CreateSemaphore,ReleaseSemaphore API)。创建信号量为空。在生产者线程中,锁定互斥锁,推送消息,解锁互斥锁,将单元释放到信号量。在消费者线程中,等待使用WFSO的信号量句柄(就像你在上面的事件上等待),然后锁定互斥锁,弹出消息,解锁互斥锁。
为什么这比事件更好?
1)无需检查队列计数 - 信号量计算消息。
2)信号量的信号不会因为没有线程在等待而“丢失”。
3)不检查队列计数意味着由于抢占而导致此类检查的结果和代码路径不能正确。
4)它将适用于多个生产者和多个消费者而无需改变。
5)它更加跨平台友好 - 所有抢占式操作系统都有互斥/信号量。
答案 2 :(得分:0)
如果有多个线程同时使用数据,或者您使用PulseEvent
而不是SetEvent
,则会很危险。
但是只有一个消费者,并且由于事件将一直发出信号直到你等到它(如果自动休息)或永远(如果手动重置),它应该可以正常工作。