为什么在 Windows 上应优先选择事件等待而非 Sleep(1)

· 更新日期: · · Windows开发, 同步, 事件, 定时器, 设计

更新记录(2 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276829)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.21615483)
首次发布
引用本文(DOI: 10.5281/zenodo.21615482)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《为什么在 Windows 上应优先选择事件等待而非 Sleep(1)》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615482 https://comcomponent.com/zh-CN/blog/2026/03/16/006-windows-timer-vs-event-wait/

DOI(最新版本)
10.5281/zenodo.21615482
DOI(此版本)
10.5281/zenodo.22281997

在上一篇Windows 软实时实践指南中,我写过要避免把周期交给 Sleep 的循环。 这次只聚焦其中一点来梳理:为什么应当优先选择 event wait,而不是短时的 timer wait。

本文可以单独阅读。 上一篇的结论只有一句:“把周期交给 Sleep 的观望型循环,既不保证等待时长也不保证唤醒时机,所以不要把它当作周期处理的基础”。只要记住这一点,后面的内容不读上一篇也能跟上。

在 Windows 上,如果用 Sleep(1) 或带短 timeout 的 wait 来设计“每隔一定时间看一下”的逻辑,就必然会受到 system clock 粒度以及随后的调度延迟的影响。 在普通设置下,往往要以 15.6ms 级别的 platform timer resolution 为前提,所以即使本意是“1ms 后再看一次”,实际上也很容易变成相当粗糙的等待。

另一方面,如果真正想等待的是任务到达、I/O 完成、停止请求、状态变化这类“事件”而不是“时间”,就没有必要按固定间隔去查看。 由事件发生的一方 signal,等待的一方等待 event,这种方式对延迟、CPU 和功耗都更友好。

事件驱动的等待方式本图说明如果真正想等待的不是时间而是事件,就不必按固定间隔去查看,由事件发生的一方 signal、等待的一方等待 event,这种方式对延迟、CPU 和功耗都更友好。想等待的是事件发生的一方 signal等待的一方等待 event对延迟、CPU、功耗都更友好

图1:要等待的是事件时,不必按固定间隔观望,而是让发生的一方来通知。

本文想回答的问题有以下 4 个。

  • 为什么 Sleep(1) 和短时 timer wait 没有想象中那么精确
  • 为什么 event wait 不容易受到这种限制
  • 在什么场景下应该选择 event 而不是 timer
  • 即便如此,仍应该使用 timer 的场景是什么

本文中出现的术语

这里先列出正文中不加说明就直接出现的缩写。

术语 含义
platform timer resolution / system clock resolution 操作系统更新时间的间隔。timed wait 的 timeout 判定会被这个粒度牵制
ISR (Interrupt Service Routine) 中断发生时以最高优先级运行的处理。它运行期间,我们这边的 thread 只能被迫等待
DPC (Deferred Procedure Call) ISR 为了“稍后再继续”而排入的高优先级延迟处理。在本文中,把它和 ISR 一起理解为中断处理带来的延迟因素就够了
IOCP (I/O Completion Port) Windows 的一种机制:把异步 I/O 的完成通知汇总到队列中,由专用的 thread 组来接收
WaitOnAddress 用于“等待某个内存地址上的值发生变化”的同步 API。仅限同一进程内使用(5.3 会讲到)
signal(发信号) 让等待一方的条件得到满足。对 event 来说就是调用 SetEvent

1. 先说结论

  • 如果要等待任务到达或 I/O 完成,最好等待 event 而不是 timer。
  • Windows 的 timed wait 终究会受到 system clock 粒度的影响。
  • Sleep(1) 并不意味着“精确地在 1ms 后醒来”。
  • 而且即便 timeout 已经过去,thread 也只是先变为 ready 状态,并不保证立即执行。
  • 所以,“本来是在等待事件,却用 timer 去反复查看”的设计,无论对延迟还是功耗都不利。
  • 使用 timer 的场景,最好严格限定在真正以时间本身作为条件的情况,这样更清爽。

用实务中的说法来讲,大致就是这样:

  • “每 5 秒发送一次 metrics” -> timer 的工作
  • “队列中有任务到达就立刻处理” -> event / semaphore / condition variable / WaitOnAddress 的工作
  • “I/O 完成后继续执行后续处理” -> completion / event 的工作
  • “收到停止请求就停止” -> stop event / cancellation 的工作
timer 与 event 的划分本图说明只有在真正以时间本身为条件时才使用 timer,而等待任务到达、I/O 完成、停止请求这类事件时应当等待 event。时间本身事件想等待的是时间还是事件timer 的工作等待 event 的工作任务到达 / I/O 完成 / 停止请求

图2:timer 只限于以时间本身为条件的场合,事件则用 event 来等待。

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 21 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

2. 问题出在哪里

2.1 timed wait 受制于 system clock 的粒度

Windows 的 wait functions 的 timeout 精度依赖于 system clock resolution。 Sleep 也是一样,指定的毫秒数并不保证会“原样”成为等待长度。

这里重要的是,即使指定了 1ms,也不代表一定会在 1ms 后醒来。

确认自己环境的粒度

“15.6ms 级别”只是一般性的说法,手头的实际值还是自己看一下更快。确认方法有 2 种。

一种是调用 GetSystemTimeAdjustment。第 2 个参数 lpTimeIncrement 会以 100 纳秒为单位返回系统更新 time-of-day clock 的间隔。如果是 15.6ms 级别,得到的值就在 15 万上下。

#include <windows.h>
#include <cstdio>

int main()
{
    DWORD adjustment = 0;
    DWORD increment = 0;
    BOOL adjustmentDisabled = FALSE;

    if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled))
    {
        std::printf("GetSystemTimeAdjustment failed. GetLastError=%lu\n", GetLastError());
        return 1;
    }

    // increment 的单位是 100ns,换算成 ms 再看
    std::printf("time increment = %lu (100ns) = %.4f ms\n",
                increment,
                increment / 10000.0);
    return 0;
}

另一种是运行 Sysinternals 的 ClockRes。它是一个小工具,内部调用的同样是 GetSystemTimeAdjustment,只是把 system clock 的分辨率,也就是应用能够获得的最大 timer resolution 显示出来。不想写代码就想确认时,用它更快。

另外,文档中把 lpTimeIncrement 描述为系统在启动时确定的固定值,运行期间不会改变。也就是说,它是用来了解“这台环境的原始粒度”的值,而不是用来测量调用 timeBeginPeriod 之后的结果。关于后者会在 6.3 提到。

确认原始粒度的两种方法本图说明可以调用 GetSystemTimeAdjustment 查看以 100 纳秒为单位的更新间隔,或者运行内部调用同一 API 的 Sysinternals ClockRes,来确认自己环境的 timer 粒度。想知道手头环境的粒度调用 GetSystemTimeAdjustment运行 ClockRes返回以 100 纳秒为单位的更新间隔内部调用的是同一个 API

图3:写代码调用,或者使用 ClockRes,两种方式都能确认环境的原始粒度。

2.2 即使期限已到,也不一定会立即执行

更麻烦的是,timeout 过去的那一瞬间,thread 并不会立刻被执行。

正如 Sleep 的说明所写,等待时间结束后 thread 只是变为 ready 状态,并不保证马上就能获得 CPU 并运行。 它还会受到其他 thread、priority、CPU 的 idle state、DPC / ISR、lock 争用等因素的影响。

也就是说,短时 timer wait 至少存在两个层面的不确定性。

  1. timeout 的判定本身就会受 timer 粒度的牵制
  2. timeout 之后,何时开始执行取决于 scheduler
短时 timer wait 的两层不确定性本图说明短时 timer wait 中 timeout 的判定本身会被 timer 粒度牵制,而且 timeout 之后 thread 也只是变为 ready,执行开始时间取决于 scheduler,因此存在两层不确定性。开始短时 timer waittimeout 判定依赖 timer 粒度thread 只是变为 ready执行开始取决于 scheduler受其他 thread 和 DPC / ISR 影响

图4:在 timeout 判定和执行开始这两处,都会偏离指定的等待时间。

2.3 Sleep(1) 并不意味着以 1ms 为周期

看到 Sleep(1),很容易让人觉得这是一个“每 1ms 转一圈”的 loop。 但实际上不能这样理解。

while (!g_stop)
{
    Step();
    Sleep(1);
}

这个 loop 的实际情况是这样的:

  • 每一圈都会加上 Step() 的执行时间
  • Sleep(1) 本身的等待时间会受到粒度的牵制
  • 即使醒来,也不一定能立刻运行
Sleep(1) 循环的实际情况本图说明 Sleep(1) 的循环会每圈叠加 Step 的执行时间,等待时间本身又受粒度牵制,而且醒来后不一定能立刻运行,因此不构成 1 毫秒的周期。每圈叠加 Step 的执行时间不会成为 1 毫秒的周期等待受粒度牵制醒来后不一定能立刻运行

图5:三种偏差叠加在一起,因此 Sleep(1) 不会形成每 1 毫秒一次的周期。

3. 为什么事件等待更有优势

3.1 等待的结束条件从“超时”变成“signal”

event wait 之所以更有优势,是因为它改变了等待本身的含义。

timer wait 是这样的:

  • 即使什么都还没发生
  • 时间一到就醒来
  • 醒来之后再去确认“是否发生了什么”

event wait 是这样的:

  • 由事情发生的一方去 signal
  • 一旦被 signal,等待就被满足
  • 醒来的那一刻,就已经有明确的理由

画成图就能看出,等待的结束方式本身就不一样。

event wait:因为发生了才被叫醒发生的一方 signal等待醒来时理由已经确定处理timer wait:因为时间到了才醒来否是按 timer 粒度醒来等待是否发生了什么?处理

图6:只有 timer wait 一侧存在空转的循环,event wait 在醒来时理由就已经确定。

只有 timer wait 这一侧,存在空转一圈再返回的循环。这一点在 latency 和功耗两方面都会体现出来。

3.2 根据想等待的内容来选择工具

那么实际该选哪种工具?最初的判断,大致靠下面这张表就够了。

想等待的内容 不好的做法 首选方案
队列中有任务进入 用 Sleep(1) 反复 TryPop event / semaphore
I/O 完成 用 timer 去查看状态 overlapped I/O 的 event / IOCP
收到停止请求 每 100ms 检查一次 stop flag stop event / cancellation
同一进程内的值变化 while (flag == 0) Sleep(1) WaitOnAddress
时刻到达 强行凑到 event 上 timer / waitable timer

3.3 event 也并非万能

event wait 的优势在于不必按 timer 粒度醒来,但这并不意味着被 signal 的瞬间就一定零延迟地运行。

即使是 event wait,也会受到以下因素的影响:

  • scheduler latency
  • thread priority
  • CPU 的 power state
  • lock 争用
  • page fault
  • DPC / ISR

不过至少可以去掉“一直睡到下一次 timer tick”这种多余的等待方式。

event wait 能去掉的等待与仍然存在的影响本图说明即使使用 event wait,仍会受到 scheduler latency、thread priority、DPC / ISR 等影响,但可以去掉一直睡到下一次 timer tick 这种多余的等待方式。用 event wait 等待scheduler 等因素的影响仍在可去掉睡到 timer tick 的等待被 signal 后并非零延迟

图7:event 并非万能,但至少可以确定不必再按 timer 粒度醒来。

4. 典型的反模式

4.1 用 Sleep(1) 轮询队列

最常见的写法就是这个。

for (;;)
{
    if (g_stop)
    {
        break;
    }

    WorkItem item;
    if (TryPop(item))
    {
        Process(item);
        continue;
    }

    Sleep(1);
}

这种写法看似简单,但存在 3 个问题。

  1. 即使 queue 为空,也会定期醒来
  2. latency 会受到 timer 粒度的牵制
  3. 在 power 方面也是一种损耗
Sleep(1) 轮询的三个问题本图说明用 Sleep(1) 轮询队列会带来三个问题:即使队列为空也会定期醒来、延迟受 timer 粒度牵制、在功耗上也是一种损耗。用 Sleep(1)观望队列队列为空也会定期醒来latency 受粒度牵制power 上也吃亏

图8:看似简单的轮询循环,在唤醒、延迟、功耗三方面都有损耗。

4.2 用 Thread.Sleep(1) / Task.Delay(1) 监视状态

在 C# / .NET 中也会散发同样的味道。

while (!stoppingToken.IsCancellationRequested)
{
    if (_queue.TryDequeue(out WorkItem? item))
    {
        await ProcessAsync(item, stoppingToken);
        continue;
    }

    await Task.Delay(1, stoppingToken);
}

即使外表是温和的 async,设计的本质仍然是 polling。

5. 应该这样改

5.1 由 producer 在任务到达时 signal

如果等待的是队列到达,就把 polling 改成 producer 主动 signal 的形式。

  • producer 把 item 放入 queue
  • 放入 item 之后立刻 SetEvent
  • consumer 用 WaitForSingleObject 或 WaitForMultipleObjects 等待
  • 醒来后就 drain queue
producer 发出 signal 的形式本图说明 producer 把 item 放入队列之后立刻 SetEvent,consumer 用 WaitForSingleObject 等待,醒来后 drain 队列的流程。consumerqueueproducerconsumerqueueproducer放入 item随后立刻 SetEvent从 wait 中醒来drain queue

图9:由知道到达情况的 producer 一方来通知,consumer 的空转就消失了。

5.2 用 WaitForMultipleObjects 同时等待 work 和 stop

对于简单的 worker,这种写法比较清晰。

HANDLE waits[2] = { _stopEvent, _workEvent };  // index 0 = stop, index 1 = work

for (;;)
{
    // bWaitAll = FALSE,所以返回值是“最先被 signal 的 handle 的 index”
    DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);

    // 失败是 WAIT_FAILED ((DWORD)0xFFFFFFFF)。原因只能靠 GetLastError 得知
    if (rc == WAIT_FAILED)
    {
        throw std::system_error(
            static_cast<int>(GetLastError()),
            std::system_category(),
            "WaitForMultipleObjects failed.");
    }

    if (rc == WAIT_OBJECT_0)  // stop
    {
        return;
    }

    if (rc == WAIT_OBJECT_0 + 1)  // work
    {
        DrainQueue();
        continue;
    }

    // 在 INFINITE 等待下走到这里属于预期之外
    //(WAIT_TIMEOUT 或 WAIT_ABANDONED_0 之类)。不要吞掉,直接抛出
    throw std::runtime_error("WaitForMultipleObjects returned an unexpected value.");
}

这个例子的要点有 3 个。

  • Sleep(1) 已经消失
  • item 到达时由 producer 调用 SetEvent
  • worker 同时等待 stop 和 work

关于返回值的处理,再补充几个实务中容易失手的地方。

  • 当 bWaitAll 为 FALSE 时,成功的返回值处于 WAIT_OBJECT_0 到 WAIT_OBJECT_0 + nCount - 1 的范围内,从中减去 WAIT_OBJECT_0 得到的就是数组的 index。如果只写 == WAIT_OBJECT_0 和 != WAIT_OBJECT_0 + 1 这两个分支,一旦 handle 增加到 3 个就会立刻出问题
  • 多个对象同时被 signal 时,返回的是 index 较小的那一个。上面的例子把 stop 放在 index 0,就是为了不漏掉停止请求
  • 失败不是以异常,而是以 WAIT_FAILED((DWORD)0xFFFFFFFF) 这个返回值返回的。原因不调用 GetLastError 就无从得知。如果把 rc != 期望值 一律写成“失败”,handle 已被关闭、没有 SYNCHRONIZE 权限之类的原因就被抹掉了
  • 如果把 mutex 也混入等待对象,还可能返回 WAIT_ABANDONED_0 系列的值。这个例子只等待 event,所以按预期之外处理

5.3 同一进程内也可以考虑 WaitOnAddress

如果只是在同一个进程内“等待某个值发生变化”,WaitOnAddress 也是相当有力的选择。 它省去了创建并初始化 event、还要照看值与同步不要错位的那些麻烦。

选用时的感觉大致是这样。

对比项 event / semaphore / waitable object WaitOnAddress
等待对象的范围 也可跨进程,还可以命名 仅限同一进程内
唤醒一方 SetEvent / ReleaseSemaphore 等 WakeByAddressSingle / WakeByAddressAll
前期准备 需要创建内核对象并管理 handle 有一个要等待的变量就够了
可用版本 很早以前就可以使用 Windows 8 / Windows Server 2012 及以后
链接库 Kernel32.lib Synchronization.lib

使用时有 3 点不希望搞错。

  1. 必须和 WakeByAddressSingle 或 WakeByAddressAll 成对使用。 改写值的一方如果不调用它们,等待中的 thread 就不会醒来。只唤醒一个用 Single,全部唤醒用 All
  2. WaitOnAddress 即使没有被 signal 也可能返回。 文档中也明确写了在低内存等状态下可能提前醒来。返回之后必须重新读一次值,确认它是否真的变了,因此要写成 while 循环
  3. 可等待的大小只能是 1 / 2 / 4 / 8 字节中的一种
  4. 标志必须是原子的。WakeByAddressSingle 只是唤醒等待中的 thread,并不会让此前的写入变成原子操作,也不保证其可见性。用裸变量从两边读写,在 C++ 中就是数据竞争(未定义行为),在优化过的构建中可能看不到更新而一直停住。写入一方用 release,读取一方用 acquire,两边配套
// 等待方和唤醒方是不同的线程,所以标志必须是原子的。
// 用裸 ULONG 从两边读写,在 C++ 中就是数据竞争(未定义行为),
// 在优化过的构建中,值可能一直留在寄存器里而看不到更新,
// 即使被唤醒也可能继续停住
std::atomic<ULONG> g_ready{ 0 };
static_assert(std::atomic<ULONG>::is_always_lock_free,
              "要传给 WaitOnAddress,因此必须是无锁的");

// “等到 g_ready 不再是 0 为止”的最小写法
ULONG undesired = 0;
ULONG captured = g_ready.load(std::memory_order_acquire);

while (captured == undesired)
{
    // 有可能提前返回,所以返回后必须重新读取
    WaitOnAddress(&g_ready, &undesired, sizeof(ULONG), INFINITE);
    captured = g_ready.load(std::memory_order_acquire);
}

改写值的一方,在更新值之后再唤醒。

// 用 release 写入。这样写之后,在这一行之前准备好的数据(下面的 payload)
// 也一定能被用 acquire 读取的一方看到。保证顺序的是这个 store,
// 而不是 WakeByAddressSingle ── 它只是唤醒正在等待的 thread,
// 既不会让此前的写入变成原子操作,也不保证其可见性
g_payload = ...;                                  // 想一并传递的数据
g_ready.store(1, std::memory_order_release);
WakeByAddressSingle(&g_ready);
WaitOnAddress 的成对流程本图说明改写方先准备数据、用 release 写入标志再用 WakeByAddressSingle 唤醒,等待方则考虑到提前返回,用 acquire 重新读取值的 while 循环来等待,二者成对配合。等待的一方原子标志改写的一方等待的一方原子标志改写的一方用 acquire 读取用 WaitOnAddress 等到变化用 release 写入WakeByAddressSingle醒来后重新读取

图10:负责唤醒的是 WakeByAddress,保证值可见性的则是 release 与 acquire 这一侧。

6. 即便如此仍应使用 timer 的场景

6.1 当时间本身就是条件时

当然,确实存在应该使用 timer 的场景。

  • 每 5 秒发送一次 metrics
  • 200ms 之后进行 retry
  • 每 1 分钟清理一次缓存
  • 等到期限时刻再判定为 timeout

在这些场景中,想等待的确实就是时间。

6.2 使用 waitable timer

在 Windows 上要等待“时间本身”时,与其随手堆叠 Sleep,不如使用 waitable timer,语义会更清楚。

6.3 不要把 timeBeginPeriod 当作常规手段

一在意短时 timer wait 的精度,就很容易想加上 timeBeginPeriod(1)。 但最好不要把它当作常规的首选方案。

理由有 3 个。

  1. 存在 power / performance 方面的成本
  2. 在较新版本的 Windows 上,行为会稍微复杂一些
  3. 多数情况下并没有解决根本原因
不把 timeBeginPeriod 当作常规手段的理由本图说明即使在意短时 timer wait 的精度,也最好不要把 timeBeginPeriod 当作常规首选,因为它有 power 和 performance 成本,在较新版本的 Windows 上行为稍微复杂,而且多数情况下并没有解决根本原因。在意精度想加上 timeBeginPeriod不当作常规的首选存在成本行为稍微复杂没有解决根本原因

图11:在提升精度之前,先回想一下不宜常用的三个理由。

7. 代码审查时的检查清单

  • 是否用 Sleep(1) / Thread.Sleep(1) / Task.Delay(1) 写出了观望型 loop
  • 是否本来在等待 queue 到达、I/O 完成或停止请求,却用 timer 去 poll
  • 是否设计成可以由 producer / completion 一侧发出 signal
  • 能否把 stop 和 work 合并到一次 wait 中一起等待
  • 如果是同一进程内的值变化,能否改用 WaitOnAddress 来写
  • 在使用 timer 的地方,真正想等待的是否确实是“时间”

8. 总结

在 Windows 上,用短时 timer wait 实现“每隔一定时间看一下”的设计,终究会受到 timer 粒度和 scheduler 的影响。 因此,Sleep(1) 或短 timeout 并不像表面看起来那样是精确的等待。

另一方面,如果真正想等待的是任务到达、I/O 完成、停止请求、状态变化这类“事件”,那么 event wait 更自然。

归纳起来,就是这一句话:

要等待时间就用 timer,要等待事件就用 event。

只要把这条界线划清楚,

  • latency 变得更容易预估
  • 多余的 periodic wakeup 减少
  • 在 power 方面也会有所改善
  • 代码的意图更容易理解

这些效果就会体现出来。

时间用 timer,事件用 event本图说明只要把等待时间就用 timer、等待事件就用 event 这条界线划清楚,latency 就更容易预估,多余的 periodic wakeup 会减少,功耗方面也会改善,代码的意图也更容易理解。时间事件想等待的是哪一种用 timer 等待用 event 等待界线变得清晰latency 更容易预估多余的 wakeup 减少

图12:只要守住这一行界线,延迟、功耗以及代码意图的可读性都会改变。

9. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

Windows 的 Sleep(1) 为什么不会精确地在 1 毫秒后醒来?
因为 Windows 的 timed wait 的 timeout 精度依赖于 system clock resolution,在普通设置下往往要以 15.6 毫秒级别的 platform timer resolution 为前提。而且即使等待时间结束,thread 也只是变为 ready 状态,并不保证能立刻获得 CPU 并运行。它还会受到其他 thread、priority、CPU 的 idle state、DPC/ISR、lock 争用等因素的影响。也就是说,短时 timer wait 至少存在两个层面的不确定性:timeout 的判定本身会受 timer 粒度牵制,以及 timeout 之后的执行开始时间取决于 scheduler。
用 Sleep(1) 或 Task.Delay(1) 轮询队列的设计有什么问题?
问题有 3 个:即使队列为空也会定期醒来、延迟会受 timer 粒度牵制、在功耗上也是一种损耗。C# 中 await Task.Delay(1) 的循环外表看起来很温和,但设计的本质仍然是 polling。修正方法是改为由 producer 在把 item 放入队列之后立刻 SetEvent,consumer 则用 WaitForSingleObject 或 WaitForMultipleObjects 等待。把 stop 事件和 work 事件合并到一次 wait 中一起等待,还能让程序对停止请求立刻做出反应。
计时器等待和事件等待应该如何区分使用?
划分原则是:要等待时间就用 timer,要等待事件就用 event。像每 5 秒发送一次 metrics 这种以时间本身为条件的处理,属于 waitable timer 的工作。队列中任务的到达适合用 event 或 semaphore,I/O 完成适合用 overlapped I/O 的 event 或 IOCP,停止请求适合用 stop 事件或 cancellation,同一进程内的值变化则适合用 WaitOnAddress。把这条界线划清楚之后,延迟会变得更容易预估,不必要的 periodic wakeup 也会减少,代码的意图也会更清晰。
用 timeBeginPeriod(1) 提升计时器精度不就能解决问题了吗?
最好不要把它当作常规的首选方案。理由有 3 个:存在 power 和 performance 方面的成本;在较新版本的 Windows 上行为会稍微复杂一些;而且多数情况下并没有解决根本原因。如果真正想等待的是任务到达或 I/O 完成这类事件,与提升计时器精度相比,改为由触发方 signal 的事件驱动设计,对延迟、CPU 和功耗都会更加友好。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表