产品/消费者 - 什么是最佳信号模式

时间:2013-01-02 20:01:11

标签: c++ windows performance synchronization

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

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下有一种更快的方法。即使C没有睡觉,也可以调用 wake_thread 。这导致很多对SetEvent的调用什么都不做。是否有更快的方法来使用手动重置事件和引用计数器来确保仅在实际设置某些内容时调用SetEvent。

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

3 个答案:

答案 0 :(得分:2)

SetEvent()会引入一些延迟,因为它必须进行系统调用(sysenter触发从用户到内核模式的切换),以便对象管理器检查事件的状态并发送它(通过致电KeSetEvent())。我认为系统调用的时间可能被认为是可以接受的,即使在你的情况下,但这是猜测。可能会引入大部分延迟的地方是事件的接收方。换句话说,从WaitFor*Object()唤醒线程需要花费时间,而不是向事件发出信号。 Windows调度程序尝试通过为具有等待返回的线程提供优先级“提升”来帮助更快地到达该线程,但该提升仅执行了这么多。

为了解决这个问题,您应该确保只在必要时才等待。执行此操作的典型方法是,在您的消费者中,当您发出信号时,请在不等待事件的情况下使用您可以使用的每个工作项,然后在完成后拨打sleep_thread()

我应该指出,SetEvent() / WaitFor*Object()几乎肯定比没有吃100%CPU的速度更快,即便如此,由于任何锁定对象需要争用,它可能会更快保护您的共享数据。

通常情况下,我会建议使用ConditionVariable但我没有测试其性能与您的技术相比。我怀疑它可能会更慢,因为它也有输入CRITICAL_SECTION对象的开销。您可能必须测量不同的性能 - 当有疑问时,测量,测量,测量。

我能想到的另一件事就是MS确实承认调度和等待事件可能很慢,特别是当它重复执行时。为了解决这个问题,他们将CRITICAL_SECTION对象更改为在用户模式下多次尝试在实际等待事件之前获取锁定。他们称之为spin count。虽然我不推荐它,但你可以做类似的事情。

答案 1 :(得分:2)

我没有测试过这个(除了确保它编译),但我认为这应该可以解决问题。不可否认,这比我最初想的要复杂一点。请注意,您可以进行一些明显的优化;为了清晰起见,我将其保留为未经优化的形式,以及为可能需要的任何调试提供帮助。我也省略了错误检查。

#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函数,这使得事情有点尴尬。如果同步代码被移入队列实现中,它可能会更自然,更有效,并且几乎肯定更清晰。

答案 2 :(得分:0)

类似的东西:

void consumer_thread(void)
{
   while(1)
   {
      WaitForSingleObject(...);

      // Consume all items from queue in a thread safe manner (e.g. 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, e.g. a spin lock.  
   // This guarantees you will never miss the "edge"
   if( queue_was_empty )
   {
      SetEvent(...);
   }
}

一般的想法是只将过渡上的SetEvent从空变为满。如果线程具有相同的优先级,Windows应该让生产者继续运行,因此您可以最小化每个队列插入的SetEvent调用数。我发现这种安排(在相同优先级的线程之间)以提供最佳性能(至少在Windows XP和Win7,YMMV下)。