4

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

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 周期都很重要。

4

3 回答 3

2

SetEvent()将引入一些延迟,因为它必须进行系统调用(sysenter触发从用户模式到内核模式的切换),以便对象管理器检查事件的状态并调度它(通过调用KeSetEvent())。我认为即使在您的情况下,系统调用的时间也可能被认为是可以接受的,但这是推测。可能会引入大部分延迟的地方是事件的接收方。换句话说,从 a 中唤醒线程WaitFor*Object()比发出事件信号要花费时间。Windows 调度程序试图通过将优先级“提升”给等待返回的线程来帮助更快地到达线程,但这种提升只能起到这么多的作用。

为了解决这个问题,您应该确保仅在必要时等待。执行此操作的典型方法是,在您的使用者中,当您收到信号时,使用您可以使用的每个工作项,而无需再次等待事件,然后在完成后拨打电话sleep_thread()

我应该指出,SetEvent()/WaitFor*Object()几乎肯定比不消耗 100% CPU 的所有东西都快,即使这样,由于任何锁定对象需要保护您的共享数据,它也可能会更快。

通常,我会推荐使用ConditionVariable但与您的技术相比,我没有测试过它的性能。我怀疑它可能会更慢,因为它也有输入 CRITICAL_SECTION 对象的开销。您可能必须以不同的方式衡量绩效——当有疑问时,衡量、衡量、衡量。

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

于 2013-01-02T20:52:34.377 回答
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 函数,这让事情有点尴尬。如果将同步代码移到队列实现中,它可能会更自然、更高效,并且几乎可以肯定更清晰。

于 2013-01-03T02:24:54.517 回答
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 下)。

于 2013-01-03T02:41:51.800 回答