在WaitForSingleObject()和SetEvent()函数之间进行同步的算法

The algorithm for synchronizing between WaitForSingleObject() and SetEvent() functions?

本文关键字:同步 算法 函数 WaitForSingleObject SetEvent 之间      更新时间:2023-10-16

我想为两个线程实现一个消息队列。线程#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()之前调用(注意(*)),这将是危险的。

您的代码很好!

SetEvent和WaitForSingleObject之间的计时没有实际问题:关键问题是事件上的WaitForSingleObject将检查事件的状态,并等待它被触发。如果事件已经被触发,它将立即返回。(从技术角度来说,它是级别触发的,而不是边缘触发的。)这意味着,如果在调用WaitForSingleObject之前或期间调用SetEvent,那就没问题;在任何一种情况下,WaitForSingleObject都将返回;立即或稍后调用SetEvent时。

(顺便说一句,我假设在这里使用自动重置事件。我想不出使用手动重置事件的好理由;在WaitForSingleObject返回后,你最终不得不立即调用ResetEvent;如果你忘记了这一点,你可能会等待一个已经等待但忘记清除的事件。此外,在检查之前重置很重要底层数据状态,否则,如果在处理数据和调用Reset()之间调用SetEvent,则会丢失该信息。坚持使用自动重置,就可以避免所有这些。)

--

[编辑:我误读了OP的代码,认为它在每次唤醒时都会发出一个‘pop’,而不是只等待空的,所以下面的注释指的是该场景的代码。OP的代码实际上相当于下面建议的第二个修复程序。因此,下面的文本实际上描述了一个有点常见的编码错误,事件被当作信号量使用,而不是OP的实际代码。]

但这里有一个不同的问题[或者,如果每次等待只有一个弹出…],那就是Win32事件对象只有两种状态:无信号和有信号,所以你只能使用它们来跟踪二进制状态,而不能计数。如果您的SetEvent和已经发出信号的事件,它将保持signaled,并且额外的SetEvent调用的信息将丢失。

在这种情况下,可能发生的情况是:

  • 项被添加,SetEvent被调用,事件现在被发出信号
  • 添加了另一项,再次调用SetEvent,事件保持信号状态
  • 工作线程调用WaitForSingleObject,返回并清除事件
  • 仅处理一个项目
  • 工作线程调用WaitForsingleObject,即使队列中还有一个项目,它也会阻塞,因为事件没有信号

有两种方法可以解决这个问题:经典的Comp.Sci方法是使用信号量而不是事件——信号量本质上是对所有"Set"调用进行计数的事件;相反,您可以将事件视为最大计数为1的信号量,该信号量忽略该信号量之外的任何其他信号。

另一种方法是继续使用事件,但当工作线程唤醒时,它只能假设队列中可能有一些项目,并且它应该在返回等待之前尝试处理所有项目——通常是将弹出项目的代码放入一个循环中,该循环会弹出项目并处理它们,直到其为空。该事件现在不用于计数,而是用于发出"队列不再为空"的信号。(请注意,当您这样做时,您还可以遇到这样的情况:在处理队列时,您也处理刚刚添加并调用了SetEvent的项目,这样,当工作线程到达WaitForSingleObject时,线程会醒来,但发现队列是空的,因为该项目已经被处理;这一开始可能看起来有点令人惊讶,但实际上很好。)

我认为这两者基本上是对等的;两者都有一些小的利弊,但都是正确的。(就我个人而言,我更喜欢事件方法,因为它将"需要做的事情"或"有更多数据可用"的概念与工作或数据的数量脱钩。)

"经典"方式(即肯定会正确工作)是使用信号量(请参阅CreateSemaphore,ReleaseSemaphoreneneneba API)。将信号量创建为空。在生产者线程中,锁定互斥体,推送消息,解锁互斥体,释放信号量的一个单元。在使用者线程中,使用WFSO等待信号量句柄(就像您等待上面的事件一样),然后锁定互斥对象,弹出消息,解锁互斥对象。

为什么这比事件更好?

1) 无需检查队列计数-信号量对消息进行计数。

2) 信号量的信号不会因为没有线程在等待而"丢失"。

3) 不检查队列计数意味着这种检查的结果和作为结果的代码路径不会因为抢占而不正确。

4) 它将适用于多个生产商和多个消费者,而不会改变。

5) 它对跨平台更友好——所有抢占式操作系统都有互斥/信号量。

如果有多个线程同时消耗数据,或者使用PulseEvent而不是SetEvent,这将是危险的。

但只有一个消费者,并且由于事件将一直保持信号,直到您等待(如果是自动休息)或永远(如果是手动重置),因此它应该可以正常工作。