产品/消费者 – 最佳的信号模式是什么?

我正在构build一个需要两个函数来同步线程的高性能应用程序

void wake_thread(thread) void sleep_thread(thread) 

该应用程序有一个单线程(让我们称之为C),可能睡着sleep_thread调用。 有多个线程将调用wake_thread。 当wake_thread返回时,它必须保证C正在运行或将被唤醒。 wake_thread一定不要阻塞。

简单的方法当然是使用这样的同步事件:

 hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); void wake_thread(thread) { SetEvent(hEvent); } 

和:

 void sleep_thread(thread) { WaitForSingleObject(hEvent); } 

这提供了所需的语义,并且没有竞争情况(只有一个线程正在等待,但多个线程可以发信号)。 我把它包括在这里来展示我正在调整的东西。

但是,我想知道在这个非常特定的情况下Windows有一个更快的方法。 wake_thread可能会被调用很多,即使C没有睡觉。 这导致大量的SetEvent调用什么都不做。 会不会有一个更快的方法来使用手动重置事件和引用计数器,以确保SetEvent仅在实际需要设置的时候被调用。

在这种情况下每个CPU周期都会计数。

我没有测试过这个(除了确保它编译),但我认为这应该做的伎俩。 无可否认,这比我起初想的要复杂一些。 请注意,您可以做出一些明显的优化; 为了清晰起见,我将其留在未优化的形式,并帮助任何可能需要的调试。 我也省略了错误检查。

 #include <intrin.h> HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); __declspec(align(4)) volatile LONG thread_state = 2; // 0 (00): sleeping // 1 (01): sleeping, wake request pending // 2 (10): awake, no additional wake request received // 3 (11): awake, at least one additional wake request void wake_thread(void) { LONG old_state; old_state = _InterlockedOr(&thread_state, 1); if (old_state == 0) { // This is the first wake request since the consumer thread // went to sleep. Set the event. SetEvent(hEvent); return; } if (old_state == 1) { // The consumer thread is already in the process of being woken up. // Any items added to the queue by this thread will be processed, // so we don't need to do anything. return; } if (old_state == 2) { // This is an additional wake request when the consumer thread // is already awake. We've already changed the state accordingly, // so we don't need to do anything else. return; } if (old_state == 3) { // The consumer thread is already awake, and already has an // additional wake request registered, so we don't need to do // anything. return; } BigTrouble(); } void sleep_thread(void) { LONG old_state; // Debugging only, remove this test in production code. // The event should never be signaled at this point. if (WaitForSingleObject(hEvent, 0) != WAIT_TIMEOUT) { BigTrouble(); } old_state = _InterlockedAnd(&thread_state, 1); if (old_state == 2) { // We've changed the state from "awake" to "asleep". // Go to sleep. WaitForSingleObject(hEvent, INFINITE); // We've been buzzed; change the state to "awake" // and then reset the event. if (_InterlockedExchange(&thread_state, 2) != 1) { BigTrouble(); } ResetEvent(hEvent); return; } if (old_state == 3) { // We've changed the state from "awake with additional // wake request" to "waking". Change it to "awake" // and then carry on. if (_InterlockedExchange(&thread_state, 2) != 1) { BigTrouble(); } return; } BigTrouble(); } 

基本上这使用手动重置事件和两位标志来重现自动重置事件的行为。 如果绘制状态图,可能会更清楚。 线程的安全性取决于哪些函数允许进行哪些转换,以及何时允许发送事件对象。

作为编辑:我认为它是同步代码分离wake_thread和sleep_thread函数,使事情有点尴尬。 如果同步代码被移入队列实现中,它可能会更自然,效率更高,而且几乎肯定会更清晰。

SetEvent()会引入一些延迟,因为它必须进行系统调用( sysenter触发从用户到内核模式的切换),以便检查事件的状态并分派它(通过调用KeSetEvent() ) 。 我认为即使在你的情况下,系统调用的时间也可以被认为是可以接受的,但这是猜测。 大部分的等待时间可能会被引入到事件的接收端。 换句话说,从WaitFor*Object()唤醒一个线程需要花费时间,而不是发信号给事件。 Windows调度程序试图通过给正在等待返回的线程提供一个优先级“提升”来帮助更早地进入线程,但是这种提升只会做很多事情。

为了解决这个问题,你应该确保你只是在需要的时候等待。 这样做的典型方法是,在你的消费者中,当你发出信号去消费时,消耗你所能得到的每一个工作项目,而不用再等待事件,那么当你完成呼叫sleep_thread()

我应该指出, SetEvent() / WaitFor*Object()几乎肯定比一切吃不到100%的CPU更快,即使这样可能会更快,因为争夺任何锁对象需要保护您的共享数据。

通常情况下,我会推荐使用ConditionVariable,但是我还没有测试过与您的技术相比的性能。 我有一个怀疑,它可能会更慢,因为它也有进入CRITICAL_SECTION对象的开销。 您可能需要衡量不同的表现 – 如有疑问,衡量,衡量和衡量。

我唯一能想到的另一件事是,MS确实承认调度和等待事件可能会很慢,特别是当它重复执行时。 为了解决这个问题,他们改变了CRITICAL_SECTION对象,在用户模式下多次尝试获取锁定,然后才等待事件。 他们把这称为旋转计数 。 虽然我不会推荐它,但你也许可以做类似的事情。

就像是:

 void consumer_thread(void) { while(1) { WaitForSingleObject(...); // Consume all items from queue in a thread safe manner (eg critical section) } } void produce() { bool queue_was_empty = ...; // in a thread safe manner determine if queue is empty // thread safe insertion into queue ... // These two steps should be done in a way that prevents the consumer // from emptying the queue in between, eg a spin lock. // This guarantees you will never miss the "edge" if( queue_was_empty ) { SetEvent(...); } } 

一般的想法是只有SetEvent从空到满的过渡 。 如果线程具有相同的优先级,那么Windows应该让生产者继续运行,因此可以最大限度地减少每个队列插入的SetEvent调用次数。 我发现这种安排(在同等优先级的线程之间)提供最好的性能(至少在Windows XP和Win7下,YMMV)。