引用本文(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 和功耗都更友好。
flowchart TB
accTitle: 事件驱动的等待方式
accDescr: 本图说明如果真正想等待的不是时间而是事件,就不必按固定间隔去查看,由事件发生的一方 signal、等待的一方等待 event,这种方式对延迟、CPU 和功耗都更友好。
d1["想等待的是事件"] --> d2["发生的一方 signal"]
d2 --> d3["等待的一方等待 event"]
d3 -.-> d4["对延迟、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 的工作
flowchart TB
accTitle: timer 与 event 的划分
accDescr: 本图说明只有在真正以时间本身为条件时才使用 timer,而等待任务到达、I/O 完成、停止请求这类事件时应当等待 event。
q1{"想等待的是时间还是事件"}
q1 -->|"时间本身"| t1["timer 的工作"]
q1 -->|"事件"| e1["等待 event 的工作"]
e1 -.-> rei["任务到达 / 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 提到。
flowchart TB
accTitle: 确认原始粒度的两种方法
accDescr: 本图说明可以调用 GetSystemTimeAdjustment 查看以 100 纳秒为单位的更新间隔,或者运行内部调用同一 API 的 Sysinternals ClockRes,来确认自己环境的 timer 粒度。
g1["想知道手头环境的粒度"] --> g2["调用 GetSystemTimeAdjustment"]
g1 --> g3["运行 ClockRes"]
g2 --> g4["返回以 100 纳秒为单位的更新间隔"]
g3 -.-> g5["内部调用的是同一个 API"]
图3:写代码调用,或者使用 ClockRes,两种方式都能确认环境的原始粒度。
2.2 即使期限已到,也不一定会立即执行
更麻烦的是,timeout 过去的那一瞬间,thread 并不会立刻被执行。
正如 Sleep 的说明所写,等待时间结束后 thread 只是变为 ready 状态,并不保证马上就能获得 CPU 并运行。
它还会受到其他 thread、priority、CPU 的 idle state、DPC / ISR、lock 争用等因素的影响。
也就是说,短时 timer wait 至少存在两个层面的不确定性。
- timeout 的判定本身就会受 timer 粒度的牵制
- timeout 之后,何时开始执行取决于 scheduler
flowchart TB
accTitle: 短时 timer wait 的两层不确定性
accDescr: 本图说明短时 timer wait 中 timeout 的判定本身会被 timer 粒度牵制,而且 timeout 之后 thread 也只是变为 ready,执行开始时间取决于 scheduler,因此存在两层不确定性。
s1["开始短时 timer wait"] --> s2["timeout 判定依赖 timer 粒度"]
s2 --> s3["thread 只是变为 ready"]
s3 --> s4["执行开始取决于 scheduler"]
s3 -.-> s5["受其他 thread 和 DPC / ISR 影响"]
图4:在 timeout 判定和执行开始这两处,都会偏离指定的等待时间。
2.3 Sleep(1) 并不意味着以 1ms 为周期
看到 Sleep(1),很容易让人觉得这是一个“每 1ms 转一圈”的 loop。
但实际上不能这样理解。
while (!g_stop)
{
Step();
Sleep(1);
}
这个 loop 的实际情况是这样的:
- 每一圈都会加上
Step()的执行时间 Sleep(1)本身的等待时间会受到粒度的牵制- 即使醒来,也不一定能立刻运行
flowchart TB
accTitle: Sleep(1) 循环的实际情况
accDescr: 本图说明 Sleep(1) 的循环会每圈叠加 Step 的执行时间,等待时间本身又受粒度牵制,而且醒来后不一定能立刻运行,因此不构成 1 毫秒的周期。
p1["每圈叠加 Step 的执行时间"] --> p4["不会成为 1 毫秒的周期"]
p2["等待受粒度牵制"] --> p4
p3["醒来后不一定能立刻运行"] --> p4
图5:三种偏差叠加在一起,因此 Sleep(1) 不会形成每 1 毫秒一次的周期。
3. 为什么事件等待更有优势
3.1 等待的结束条件从“超时”变成“signal”
event wait 之所以更有优势,是因为它改变了等待本身的含义。
timer wait 是这样的:
- 即使什么都还没发生
- 时间一到就醒来
- 醒来之后再去确认“是否发生了什么”
event wait 是这样的:
- 由事情发生的一方去 signal
- 一旦被 signal,等待就被满足
- 醒来的那一刻,就已经有明确的理由
画成图就能看出,等待的结束方式本身就不一样。
flowchart TB
subgraph TimerWait["timer wait:因为时间到了才醒来"]
T1["等待"] --> T2["按 timer 粒度醒来"]
T2 --> T3{"是否发生了什么?"}
T3 -- "否" --> T1
T3 -- "是" --> T4["处理"]
end
subgraph EventWait["event wait:因为发生了才被叫醒"]
E1["等待"] --> E2["发生的一方 signal"]
E2 --> E3["醒来时理由已经确定"]
E3 --> E4["处理"]
end
图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”这种多余的等待方式。
flowchart TB
accTitle: event wait 能去掉的等待与仍然存在的影响
accDescr: 本图说明即使使用 event wait,仍会受到 scheduler latency、thread priority、DPC / ISR 等影响,但可以去掉一直睡到下一次 timer tick 这种多余的等待方式。
e1["用 event wait 等待"] --> e2["scheduler 等因素的影响仍在"]
e1 --> e3["可去掉睡到 timer tick 的等待"]
e2 -.-> e4["被 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 个问题。
- 即使 queue 为空,也会定期醒来
- latency 会受到 timer 粒度的牵制
- 在 power 方面也是一种损耗
flowchart TB
accTitle: Sleep(1) 轮询的三个问题
accDescr: 本图说明用 Sleep(1) 轮询队列会带来三个问题:即使队列为空也会定期醒来、延迟受 timer 粒度牵制、在功耗上也是一种损耗。
a1["用 Sleep(1)观望队列"] --> b1["队列为空也会定期醒来"]
a1 --> b2["latency 受粒度牵制"]
a1 --> b3["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
sequenceDiagram
accTitle: producer 发出 signal 的形式
accDescr: 本图说明 producer 把 item 放入队列之后立刻 SetEvent,consumer 用 WaitForSingleObject 等待,醒来后 drain 队列的流程。
participant P as producer
participant Q as queue
participant C as consumer
P->>Q: 放入 item
P->>C: 随后立刻 SetEvent
C->>C: 从 wait 中醒来
C->>Q: 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 点不希望搞错。
- 必须和
WakeByAddressSingle或WakeByAddressAll成对使用。 改写值的一方如果不调用它们,等待中的 thread 就不会醒来。只唤醒一个用 Single,全部唤醒用 All WaitOnAddress即使没有被 signal 也可能返回。 文档中也明确写了在低内存等状态下可能提前醒来。返回之后必须重新读一次值,确认它是否真的变了,因此要写成 while 循环- 可等待的大小只能是 1 / 2 / 4 / 8 字节中的一种
- 标志必须是原子的。
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);
sequenceDiagram
accTitle: WaitOnAddress 的成对流程
accDescr: 本图说明改写方先准备数据、用 release 写入标志再用 WakeByAddressSingle 唤醒,等待方则考虑到提前返回,用 acquire 重新读取值的 while 循环来等待,二者成对配合。
participant W as 改写的一方
participant F as 原子标志
participant S as 等待的一方
S->>F: 用 acquire 读取
S->>S: 用 WaitOnAddress 等到变化
W->>F: 用 release 写入
W->>S: WakeByAddressSingle
S->>F: 醒来后重新读取
图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 个。
- 存在 power / performance 方面的成本
- 在较新版本的 Windows 上,行为会稍微复杂一些
- 多数情况下并没有解决根本原因
flowchart TB
accTitle: 不把 timeBeginPeriod 当作常规手段的理由
accDescr: 本图说明即使在意短时 timer wait 的精度,也最好不要把 timeBeginPeriod 当作常规首选,因为它有 power 和 performance 成本,在较新版本的 Windows 上行为稍微复杂,而且多数情况下并没有解决根本原因。
t1["在意精度"] --> t2["想加上 timeBeginPeriod"]
t2 --> t3["不当作常规的首选"]
t3 -.-> r1["存在成本"]
t3 -.-> r2["行为稍微复杂"]
t3 -.-> r3["没有解决根本原因"]
图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 方面也会有所改善
- 代码的意图更容易理解
这些效果就会体现出来。
flowchart TB
accTitle: 时间用 timer,事件用 event
accDescr: 本图说明只要把等待时间就用 timer、等待事件就用 event 这条界线划清楚,latency 就更容易预估,多余的 periodic wakeup 会减少,功耗方面也会改善,代码的意图也更容易理解。
m1{"想等待的是哪一种"}
m1 -->|"时间"| m2["用 timer 等待"]
m1 -->|"事件"| m3["用 event 等待"]
m2 --> m4["界线变得清晰"]
m3 --> m4
m4 -.-> m5["latency 更容易预估"]
m4 -.-> m6["多余的 wakeup 减少"]
图12:只要守住这一行界线,延迟、功耗以及代码意图的可读性都会改变。
9. 参考资料
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- WaitForMultipleObjects function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- WakeByAddressAll function
- GetSystemTimeAdjustment function
- ClockRes - Sysinternals
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
业务系统的编码设计 ── 商品编码・客户编码的确定方法与校验位
确定商品编码・客户编码等业务系统编码体系的实践指南。整理了有意义编码与无意义流水号的判断表、JAN・Luhn等校验位算法及C#实现、Excel开头零丢失的应对方法,直至位数溢出与迁移。
睡眠、休眠、Modern Standby 与长时间运行应用 ── 用设计防止「夜里意外停止」
本文从 S3 睡眠、休眠、Modern Standby 的区别入手,整理长时间运行的 Windows 应用为何会出现「早上一看已经停止」的原因,并讲解睡眠期间计时器与 TCP 连接的行为,以及如何用 SetThreadExecutionState 进行抑制。
Windows 应用不适合 Web 化的情形 ── 判断表与「拆分」这一现实解法
「想把公司内部的 Windows 应用改造成 Web」的需求正在增加,但对于涉及设备联动、本地文件处理、离线运行、高速录入界面的应用来说,Web 化反而可能带来成本上升与功能劣化。本文从 Windows 委托开发的实务视角,整理出适合与不适合 Web 化的判断表,并提出「不...
在桌面应用中使用 .NET Generic Host 与 BackgroundService 的理由
本文整理在 Windows 工具或常驻应用中,如何使用 Generic Host 与 BackgroundService 来梳理启动、定期处理、退出处理、日志、配置与 DI。
FileSystemWatcher 实务指南:应对遗漏通知与重复通知
本文从遗漏通知、重复通知、完成判定的陷阱、重新扫描、原子式 claim、idempotency(幂等性)等角度,整理 FileSystemWatcher 的使用方法与注意事项。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
技术咨询 & 设计评审
本文讨论等待设计、同步 primitives 的选型,以及软实时场景下延迟与功耗的取舍,与技术咨询和设计评审十分契合。
Windows 应用程序开发
在 Windows 应用或服务中把 timer polling 改为 event-driven 的设计,是直接关系到 Windows 应用开发实现质量的主题。
常见问题
汇总了咨询这一主题时常见的问题。
- 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 和功耗都会更加友好。