【问题标题】:Light event in WinAPI / C++WinAPI / C++ 中的灯光事件
【发布时间】:2017-05-29 23:02:42
【问题描述】:

WinAPI / C++ 中是否有一些轻(因此快)事件?特别是,我有兴趣在设置事件时尽量减少等待事件(如WaitForSingleObject())所花费的时间。这是一个代码示例,可以进一步阐明我的意思:

#include <Windows.h>
#include <chrono>
#include <stdio.h>

int main()
{
  const int64_t nIterations = 10 * 1000 * 1000;
  HANDLE hEvent = CreateEvent(nullptr, true, true, nullptr);
  auto start = std::chrono::high_resolution_clock::now();
  for (int64_t i = 0; i < nIterations; i++) {
    WaitForSingleObject(hEvent, INFINITE);
  }
  auto elapsed = std::chrono::high_resolution_clock::now() - start;
  double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
  printf("%.3lf Ops/sec\n", nIterations / nSec);
  return 0;
}

在 3.85GHz Ryzen 1800X 上,我每秒执行 7209623.405 次操作,这意味着平均花费 534 个 CPU 时钟(或 138.7 纳秒)来检查事件是否已设置。

但是,我想在对性能至关重要的代码中使用该事件,在该代码中,大多数时间实际上设置了事件,所以它只是对特殊情况的检查,在这种情况下,控制流会转到不是性能的代码-critical(因为这种情况很少见)。

由于安全属性和名称,我知道(使用CreateEvent 创建)的WinAPI 事件是重量级的。它们用于进程间通信。也许WaitForSingleObject() 太慢了,因为它从用户模式切换到内核模式并返回,即使设置了事件也是如此。此外,对于手动和自动重置事件,此函数的行为必须不同,并且检查事件类型也需要时间。

我知道可以使用 atomic_flag 实现快速用户模式互斥锁(自旋锁)。它的旋转循环可以用std::this_thread::yield() 扩展,以便让其他线程在旋转时运行。

对于事件,我不希望完全等效于自旋锁,因为当事件未设置时,可能需要大量时间才能再次设置。如果需要事件集的每个线程都开始旋转直到它再次被设置,那将是巨大的 CPU 电力浪费(尽管如果他们调用 std::this_thread::yield 不会影响系统性能)

所以我更喜欢一个临界区的类比,它通常只在用户模式下工作,当它意识到它需要等待(在自旋之外)时,它会切换到内核模式并等待一个繁重的同步对象像互斥锁一样。

UPDATE1:我发现 .NET 有 ManualResetEventSlim ,但在 WinAPI / C++ 中找不到等价物。

UPDATE2:因为请求了事件使用的详细信息,所以在这里。我正在实现一个可以在常规模式和维护模式之间切换的知识库。一些操作是仅维护的,一些操作是常规的,一些可以在两种模式下工作,但其中一些在维护中更快,一些在常规模式下更快。在开始时,每个操作都需要知道它是处于维护模式还是常规模式,因​​为逻辑发生了变化(或者操作根本拒绝执行)。用户可以不时请求在维护模式和常规模式之间切换。这是罕见的。当此请求到达时,旧模式下的新操作无法启动(请求失败),应用程序等待旧模式下的当前操作完成,然后切换模式。所以轻事件是这个数据结构的一部分:除了模式切换之外的操作要快速,所以需要快速设置/重置/等待事件。

【问题讨论】:

  • So I would rather like an analogy of a critical section - 为什么不使用它或者说新的 Slim Reader/Writer Locks 呢?或者你需要进程间同步?
  • 这是自动重置还是手动重置事件?是否有多个线程在等待事件?
  • windows中存在不同的同步功能,它们尽量在用户模式下完成大部分工作,只有在需要等待时才进入内核——临界区、slim rw锁、等待地址更改等。用于进程同步。当然可以实现基于联锁操作的自定义同步解决方案,并在需要等待时等待事件。但这只有在没有与您的任务匹配的情况下才有意义。因为不清楚您的真正任务以及需要同步的内容,所以现在给出更具体的建议。内核模式对象需要进入内核,这是一个繁重的操作
  • IOCP(输入/输出完成端口)?
  • @RbMm ,我不需要进程间同步。您能否详细说明(可能在代码示例的答案中)Slim Reader/Writer Lock 如何替换事件?

标签: c++ performance winapi events synchronization


【解决方案1】:

从 win8 开始,最适合您的解决方案是使用 WaitOnAddress(原位 WaitForSingleObjectWakeByAddressAll(工作方式类似于 SetEvent 用于 NotificationEvent)和 WakeByAddressSingle(工作方式类似于 SynchronizationEvent)。更多阅读 - WaitOnAddress lets you create a synchronization object

实现可以是next:

class LightEvent 
{
    BOOLEAN _Signaled;
public:
    LightEvent(BOOLEAN Signaled)
    {
        _Signaled = Signaled;
    }

    void Reset()
    {
        _Signaled = FALSE;
    }

    void Set(BOOLEAN bWakeAll)
    {
        _Signaled = TRUE;
        (bWakeAll ? WakeByAddressAll : WakeByAddressSingle)(&_Signaled);
    }

    BOOL Wait(DWORD dwMilliseconds = INFINITE)
    {
        BOOLEAN Signaled = FALSE;

        while (!_Signaled)
        {
            if (!WaitOnAddress(&_Signaled, &Signaled, sizeof(BOOLEAN), dwMilliseconds))
            {
                return FALSE;
            }
        }
        return TRUE;
    }
};

不要忘记为链接器输入添加Synchronization.lib

这个新 api 的代码非常有效,它们不会为等待(如事件)创建内部内核对象,而是为此目标使用新的 api ZwAlertThreadByThreadId ZwWaitForAlertByThreadId 特殊设计。

在 win8 之前如何自己实现?首先看微不足道-布尔变量+事件句柄。并且必须看起来像:

void Set()
{
  SetEvent(_hEvent);
   // Sleep(1000); // simulate thread innterupted here
  _Signaled = true;
}

void Reset()
{
  _Signaled = false;
  // Sleep(1000); // simulate thread innterupted here
  ResetEvent(_hEvent);
}

void Wait(DWORD dwMilliseconds = INFINITE)
{
  if(!_Signaled) WaitForSingleObject(_hEvent);
}

但是这段代码确实不正确。我们在 Set (Reset) 中执行 2 次操作的问题 - 更改 _Signaled_hEvent 的状态。并且无法从用户模式作为原子/联锁操作执行此操作。这意味着线程可以在这两个操作之间被中断。假设并发调用SetReset 中有2 个不同的线程。在大多数情况下,操作将按下一个顺序执行,例如:

  SetEvent(_hEvent);
  _Signaled = true;
  _Signaled = false;
  ResetEvent(_hEvent);

这里一切正常。但可能和下一个订单(取消注释一个 Sleep 以进行测试)

  SetEvent(_hEvent);
  _Signaled = false;
  ResetEvent(_hEvent);
  _Signaled = true;

_Signaledtrue 时,_hEvent 将处于重置状态。

您自己将其实现为原子,没有操作系统支持将不是简单的,而是可能的。但我首先要寻找这个的用法 - 是为了什么?是事件之类的行为,这正是您需要的任务吗?

【讨论】:

  • 设置/重置操作只需要相对于彼此是原子的,而不是相对于任何其他线程,因此可以通过临界区来完成。 (甚至可能没有必要;很可能只有一个特定线程会修改事件的状态。)如果您不需要支持 Windows 7,WaitOnAddress 可以为您节省一些工作。但它肯定不是 必要的。
  • ... 我认为变量应该是原子的或通过互锁操作访问,以确保其他 CPU 内核可以立即看到 _Signaled = true;。至少它需要是可变的,以确保如果 Wait() 函数被内联,编译器不会优化测试。
  • @HarryJohnston - 关于_Signaled 上的原子 - 我想我们在这里没有任何收获 - 是的,当我们同时调用 WaitSet 时可能出现的情况 - 我们已经执行了 _Signaled = true; 但是线程调用Wait 却从_Signaled 读取false 并等待事件(无论如何我们在这种情况下设置)。但即使我们对_Signaled = true; 使用原子/互锁操作,这也是可能的 - 调用Wait 的线程无论如何都可以读取_Signaled jsut 之前 我们将原子/互锁设置为@987654358 的状态@.
  • 如果经常使用lock前缀(当我们修改_Signaled)这会降低性能。如果SetReset总是会从同一个线程调用-在这个是的,_Signaled_hEvent 修改之间的中断没有问题。但是对我来说仍然不清楚这一切将如何/用于什么。说如果并发将被称为WaitResetWait 将在重置之前执行,当_Signaledtrue - 这会没关系 - 事实上_hEvent 在工作线程中做一些事情事实上已经被重置了?
  • 可能是 OP 只需要这样的代码? do { if (_bTaskToDo) DoSomething(); else WaitForSingleObject(_hEvent); } while (!bQuit);set(){ _bTaskToDo = TRUE; SetEvent(_hEvent);}reset(){ ResetEvent(_hEvent);_bTaskToDo = FALSE; }
【解决方案2】:

如果您可以放弃对 Windows 7 的支持,另一个答案非常好。

但是在 Win7 上,如果你从多个线程多次设置/重置事件,但只需要很少休眠,建议的方法很慢。

相反,我使用由临界区保护的布尔值,以及唤醒/睡眠的条件变量。

wait 方法将在SleepConditionVariableCS API 上进入内核睡眠,这是预期的,也是您想要的。

然而设置和重置方法将完全在用户模式下工作:设置单个布尔变量非常快,即在 99% 的情况下,临界区将发挥用户模式无锁的魔力。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-23
    • 1970-01-01
    • 1970-01-01
    • 2022-01-14
    • 1970-01-01
    • 2013-02-27
    • 1970-01-01
    相关资源
    最近更新 更多