睡眠、休眠、Modern Standby 与长时间运行应用 ── 用设计防止「夜里意外停止」

· 更新日期: · · 睡眠, Modern Standby, 电源管理, SetThreadExecutionState, 长期运行, 定时器, 常驻应用, C#, .NET, 故障排查, Windows开发, 技术咨询

「监控应用明明整晚都在运行,早上打开一看,图表却在凌晨 1 点就停住了」「在台式机上稳定运行了好几个月的采集工具,换成笔记本电脑之后,记录突然出现了断档」── 在关于长时间运行的业务应用的咨询中,这类症状堪称经典中的经典。查看日志会发现既没有异常也没有崩溃,只是有好几个小时的记录完整地缺失了。而元凶往往不是应用本身的 bug,而是 Windows 的电源管理。

麻烦的是,「睡眠」这个词并不只对应一种情况。传统的 S3 睡眠、休眠状态(S4),以及近年来笔记本电脑上主流的 Modern Standby(S0 low power idle),从应用的角度看,停止的方式各不相同,应对措施的效果也各不相同。尤其是 Modern Standby,由于「睡眠期间系统仍在运行」这样的宣传而容易被误解,但实际上桌面应用反而会被更主动地停止。

本文将先梳理睡眠的种类以及应用在睡眠期间的行为这些最基本的知识,然后结合实现示例和判断表,整理「不让系统睡眠」「以睡眠为前提进行设计」「在指定时刻唤醒」这三种设计思路应该如何搭配使用。

1. 先说结论

  • Windows 的睡眠方式包括传统的 S3 睡眠、休眠状态(S4)、Modern Standby(S0 low power idle),支持 Modern Standby 的机型不再支持 S1~S3。可以用 powercfg /a 确认自己的 PC 属于哪一种。123
  • 即使在 Modern Standby 期间,桌面应用也不会持续运行。DAM(Desktop Activity Moderator)会挂起桌面进程的线程(会话 0 中的服务则是被限流)。请不要以「因为是 S0,所以应用应该还在运行」这种前提来做设计。4
  • 睡眠期间线程不会执行,而且计时器截止时间的计数方式也因 API 的世代而不同。从 Windows 8 开始,相对指定的计时器与等待(SetWaitableTimer 的相对指定形式、SleepEx 等)在睡眠期间不会继续计数,剩余时间会在恢复后接着计算。56 .NET 的计时器在 .NET 10 为止,其实现方式会把睡眠时间计入(如果截止时间在睡眠期间已经到达,则会在恢复后立即触发),而从 .NET 11 开始改为不计入睡眠时间6 经过时间相关的 API 中,也混杂着计入睡眠时间的(GetTickCount、QueryPerformanceCounter,即 Stopwatch 的基础)和不计入的(QueryUnbiasedInterruptTime)。78
  • 在睡眠期间变为空闲状态的 TCP 连接,有时会被NAT、防火墙、负载均衡器等中间设备因空闲超时而悄悄丢弃(例如 Azure Load Balancer 的默认行为是 4 分钟后无通知直接丢弃)。设计时应假定恢复后需要重新连接。94
  • 抑制睡眠的正规做法是 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)。不过它只对因无操作超时引发的自动睡眠有效,无法阻止用户按下电源按钮或合上笔记本盖子所触发的睡眠。应只在必要的区间内设置,结束后务必清除。1011
  • 可以用 powercfg /requests 确认抑制是否生效。如果使用 PowerCreateRequest 系列 API,还可以为请求附加理由字符串,并显示在这份列表中,这对运维排查很有帮助。121314
  • 如果要以睡眠为前提进行设计,在 .NET 中可以通过 SystemEvents.PowerModeChanged 检测,在 Win32 中则可以通过 WM_POWERBROADCAST(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC)检测。挂起通知的处理宽限期每个应用大约只有 2 秒,而且在电池电量紧张时,系统有时会不发出通知就直接进入睡眠。15161718
  • 如果要在指定时刻可靠地运行任务,首选方案是任务计划程序中的「执行任务时唤醒计算机」(WakeToRun)。它会让系统保持唤醒状态,直到任务完成为止。不过,这依赖于电源选项中的唤醒计时器许可设置,因此必须在实际设备上验证是否真的能唤醒。1920

2. Windows 的睡眠并非只有一种

首先,先掌握作为后续对策前提的三种状态。

状态 俗称 内容 从应用的角度看
S3 传统睡眠 CPU 停止,仅 RAM 通电以保持状态 完全不执行任何计算处理1
S4 休眠状态 将内存内容写入休眠文件后断电 同上。恢复时从文件还原1
S0 low power idle Modern Standby 系统以低功耗方式部分保持运行,可瞬间恢复 桌面应用会被 DAM 停止24

S3 和 S4 都是「系统不执行任何计算任务、外观上处于关机状态」。1 而 Modern Standby 则是一种更接近智能手机电源模型的方式,在屏幕熄灭期间仍保持网络连接,以低功耗状态待机,并能在按下电源按钮后不到 1 秒内恢复。支持 Modern Standby 的机型不会使用 S1~S3。221

这里的关键在于 Modern Standby 机型上搭载的DAM(Desktop Activity Moderator)。DAM 是在系统进入待机时,把桌面应用的执行抑制到与 S3 睡眠等效程度的机制:交互式会话中的进程会全部线程被挂起,而会话 0 中的服务则会被限流(大部分时间处于挂起状态,只间歇性地执行)。4 也就是说,「既然是 Modern Standby,睡眠期间应用应该还在运行」这种期待恰恰是相反的:从应用的角度看,它和 S3 一样会停止运行,而且与 S3 不同的是,由于「系统本身在运行,只有应用被停止」,这种情况会表现为时间偏差或计时器行为的不一致——安全起见,最好这样来理解它。4

要确认自己的 PC(或客户现场的 PC)属于哪种方式,不需要管理员权限,用下面的命令即可确认。3

> powercfg /a
以下睡眠状态在此系统上可用:
    待机(S0 低功耗空闲) 已连接网络
    休眠
    ...

如果显示为「待机(S3)」,就是 S3 机型;如果显示为「S0 低功耗空闲」,就是 Modern Standby 机型。遇到「换成笔记本电脑之后记录开始出现缺失」这类咨询时,首先要确认的就是这一点。

3. 睡眠期间与恢复后,应用会发生什么

3.1. 线程与计时器

睡眠期间(S3/S4,以及 DAM 挂起期间)线程不会执行。14 容易被忽视的是,计时器或等待的「截止时间」究竟如何计算睡眠时间,这一点因 API 所处的层级不同而有所区别

  • Win32 的相对计时器(SetWaitableTimer / SetWaitableTimerEx 的相对指定形式),在 Windows 7 及更早版本中会把处于低功耗状态的时间也计入倒计时(即睡眠期间倒计时仍会继续),但从 Windows 8 开始不再计入。跨越睡眠的相对计时器,会在恢复后等待剩余时间用完才触发。5
  • 另一方面,指定超时时间的等待类 API(SleepEx / WaitForMultipleObjectsEx 等),从 Windows 8 开始也不会计入睡眠等非运行状态的时间。剩余时间会跨越睡眠继续保留。6
  • .NET 中受管计时器(System.Threading.Timer 等)的触发时机,会随运行时版本的不同而变化。这是因为 Environment.TickCount64 在 .NET 10 为止会计入睡眠时间(基于 GetTickCount64),而从 .NET 11 开始改为不计入(基于 QueryUnbiasedInterruptTime)。变更说明文档本身也提醒:「可能存在恢复后计时器不再立即触发的代码」。6

也就是说,「用间隔 10 秒的 System.Threading.Timer 进行测量的应用睡眠了 8 小时」这种情况下,8 小时份的触发无论如何都不会一次性补发,但恢复后是立即触发一次,还是等待完剩余时间后才触发,取决于 API 所处的层级以及运行时版本。无论哪种情况,睡眠期间的采样都会缺失。与其依赖「恢复后立即触发」来编写重建处理逻辑,不如像第 5 章那样,通过恢复事件显式地重新调度,这样更为稳妥。

3.2. 经过时间的测量会出现偏差

经过时间相关的 API 中,混杂着计入睡眠时间与不计入睡眠时间的两类。

API 是否计入睡眠时间
GetTickCount / GetTickCount64 计入7
QueryPerformanceCounter(即 .NET Stopwatch 的基础) 计入(standby、hibernate、connected standby)8
QueryUnbiasedInterruptTime 不计入(仅计算 working state 的时间)227
Environment.TickCount / TickCount64 .NET 10 为止计入(基于 GetTickCount64),从 .NET 11 开始改为不计入(基于 QueryUnbiasedInterruptTime)6

「用 Stopwatch 判断经过 10 秒后再取下一个样本」这样的代码,一旦跨越睡眠,就会变成「经过了 8 小时零 10 秒」;反过来,基于 TickCount 的经过时间判断,在迁移到 .NET 11 之后行为也会发生变化。把「处理所耗费的时间」交给 Stopwatch 管理,把「下一次应该运行的挂钟时刻」交给 DateTime/DateTimeOffset 管理,并在恢复事件(第 5 章)中重新校准基准,做好这样的职责划分,就不会被睡眠或运行时更新牵着走。关于短周期计时器本身的设计,可以参考「为什么在 Windows 上应优先选择事件等待而非 Sleep(1)」一文。

3.3. TCP 连接会「悄悄地」失效

睡眠期间,由于应用无法进行通信,连接会变为空闲状态。问题出在路径上的设备。NAT、防火墙、负载均衡器这类中间设备,会在超时后丢弃空闲的数据流,而在很多配置下,丢弃时并不会通知连接的任何一端,而是悄悄地断开。例如 Azure Load Balancer 的默认行为就是「达到空闲超时(默认 4 分钟)后静默丢弃数据流」。9 公司内部的路由器,以及设备端内置的 TCP 协议栈,也存在类似的超时机制。

结果就是,恢复后应用的套接字表面上依然显示正常,但下一次发送时会出错,或者在等待响应时一直卡到超时。DAM 的文档中也明确指出,需要考虑进程挂起对连接生命周期和握手过程带来的影响。4 此外,在 Modern Standby 机型上,使用电池供电时,睡眠期间的网络活动默认会被整体静止(Adaptive Connected Standby)。23 收到恢复事件后,应把连接视为可疑对象,先丢弃再重新连接,这是稳妥的做法。关于排查「看似还活着、实际已经失效」的连接问题,也可以参考「TCP 重传导致工业相机通信中断的原因与排查」一文。

4. 「不让系统睡眠」── SetThreadExecutionState 与电源请求

如果只是「测量期间的这几个小时不能让系统睡眠」,正规做法就是使用 SetThreadExecutionState。在 C# 中可以通过 P/Invoke 调用它。

using System.Runtime.InteropServices;

internal static class PowerGuard
{
    [Flags]
    private enum EXECUTION_STATE : uint
    {
        ES_CONTINUOUS       = 0x80000000,
        ES_SYSTEM_REQUIRED  = 0x00000001,
        ES_DISPLAY_REQUIRED = 0x00000002,
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);

    /// <summary>在测量开始时调用:抑制自动睡眠(必须与 End 在同一线程调用)</summary>
    public static void Begin() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS |
                                EXECUTION_STATE.ES_SYSTEM_REQUIRED);

    /// <summary>测量结束时务必调用:解除抑制(必须与 Begin 在同一线程调用)</summary>
    public static void End() =>
        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
}

需要掌握的规范如下。

  • 加上 ES_CONTINUOUS 之后,效果会一直持续,直到下一次带 ES_CONTINUOUS 重新调用为止。如果不加这个标志,只会重置一次空闲计时器,想要让效果持续,就需要定期反复调用。10
  • ES_SYSTEM_REQUIRED 用于抑制系统睡眠,ES_DISPLAY_REQUIRED 用于抑制显示器关闭。如果只是后台测量,前者就已经足够。明明可以让屏幕熄灭,却连 ES_DISPLAY_REQUIRED 也一并设置,只会白白浪费电力。10
  • 无法阻止因用户按下电源按钮或合上笔记本盖子而触发的睡眠。这个函数只对因无操作超时引发的自动睡眠有效。官方文档中也明确指出,应当尊重用户的明确操作。10
  • 系统会对调用过 SetThreadExecutionState 的线程进行计数,一旦计数归零且没有用户输入,就会进入睡眠。11 如果进程异常终止,抑制效果也会随之消失,因此不必担心会出现「设置了抑制却忘记解除,导致 PC 永远不睡眠」的情况——重启一次就能解决;但反过来说,这也意味着它无法作为应对崩溃的可靠保障。
  • 正如函数名所示,这个函数设置的是调用它的那个线程的执行状态10 设置和解除必须在同一个线程中进行。由于 async/await 的后续部分有可能在不同的线程池线程上执行,如果实现中跨越了 await 来调用 Begin()End(),解除操作就会落在与设置睡眠抑制时不同的线程上而白白落空,导致抑制效果在原线程存活期间一直残留下去。比较安全的做法是固定在同一性有保证的线程(例如 UI 线程)上调用;如果设计上确实需要跨线程,请使用后文提到的基于句柄的电源请求 API。

从 Windows 7 开始,还提供了用于同一目的、更新的 API——电源请求(PowerCreateRequest / PowerSetRequest / PowerClearRequest)。它在实务上的优势在于,创建请求时可以通过 REASON_CONTEXT 传入理由字符串;请求类型除了 PowerRequestSystemRequired 之外,还有用于抑制 Modern Standby 机型上进程挂起的 PowerRequestExecutionRequired1413 官方推荐的最佳实践是「在场景开始前立即 Set,结束后立即 Clear,并在进程退出前清理句柄」。13

不过,Modern Standby 机型存在一个重要的限制。在使用电池供电的 Modern Standby 系统上,SystemRequired/ExecutionRequired 请求会在超过睡眠超时时间后的 5 分钟被强制终止。而且无论采用哪种供电方式,只要因用户操作(电源按钮、合上笔记本盖子、开始菜单中的睡眠选项)而进入睡眠,请求都会被终止。13 也就是说,「在笔记本电脑上,即使合上盖子、即使使用电池供电也要持续运行」这一点,是无法仅靠应用自身的努力实现的。这类需求需要通过电源设置和运维手段(设置合盖不睡眠、使用 AC 供电)来保障。

可以在具有管理员权限的命令提示符中,确认抑制是否真的生效。

> powercfg /requests
SYSTEM:
[PROCESS] \Device\HarddiskVolume3\Apps\SensorLogger.exe

powercfg /requests 是用来列出正在阻止睡眠或屏幕关闭的电源请求的命令,既可以用于排查「不知为何总不睡眠的 PC」,也可以用来确认自家应用的抑制是否生效。12 反过来,也需要了解管理员可以通过 powercfg /requestsoverride 设置忽略特定进程的请求。12 设计上应当把抑制类 API 理解为一种「请求」,而不是绝对保证。

最后再谈谈「得体」的问题。如果实现中在应用运行期间从头到尾一直设置着 ES_CONTINUOUS | ES_SYSTEM_REQUIRED,就意味着一个常驻应用永久性地废掉了用户自己设置的电源方案。在笔记本电脑上会消耗电池,在共享 PC 上还会影响其他用途。原则上应当把抑制范围限定在「真正不能被打断的处理正在运行的那段区间」(官方文档中的示例也是「录制开始时设置,录制完成时清除」10)。另外,对于用户一侧的临时应对手段,还有 PowerToys Awake 这个工具,它内部也是通过同样的机制(请求执行状态的线程)运作的。24 这也可以作为一个参考,用来判断到底应该在应用中实现抑制逻辑,还是交给运维工具去处理。

5. 「以睡眠为前提」── 检测、重连与记录缺失区间

对于大多数常驻监控或采集类应用来说,与其禁止睡眠,不如与睡眠共存,这样的设计思路更合理。需要做到的是三点:「在进入睡眠前知晓」「知晓何时恢复」「恢复后重新建立状态」。

在 .NET 中,Microsoft.Win32.SystemEvents.PowerModeChanged 是入口。15

using Microsoft.Win32;

SystemEvents.PowerModeChanged += OnPowerModeChanged;

private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
    switch (e.Mode)
    {
        case PowerModes.Suspend:
            // 宽限期很短:只做刷新缓冲区、记录测量停止时刻这类最小操作
            _logger.Info("suspend at {0:O}", DateTimeOffset.Now);
            _collector.Pause();
            break;

        case PowerModes.Resume:
            // 1) 把缺失区间作为数据保留下来
            _logger.Info("resume at {0:O}", DateTimeOffset.Now);
            // 2) 假定连接已经失效,先丢弃再重新连接
            _connection.Reset();
            // 3) 以挂钟时间为基准重新计算调度,重新设置计时器
            _scheduler.Rebase(DateTimeOffset.Now);
            // 4) 完成重建后再显式恢复采集(不要一直停留在 Pause 状态)
            _collector.Resume();
            break;
    }
}

官方文档中明确提到了这个事件的两个注意点:如果消息泵没有在运转,事件就不会触发(Windows 服务中需要准备隐藏窗体之类的东西),以及由于这是一个 static 事件,如果疏于取消订阅就会导致泄漏15 在 GUI 应用中可以直接使用,但对于控制台或服务类型的采集应用,则需要自行准备一个接收 Win32 WM_POWERBROADCAST 的消息窗口,或者使用无需 HWND 就能接收回调的 PowerRegisterSuspendResumeNotification(在 DAM 环境下的通知也是走这条路径)。416

下面也一并了解一下 Win32 层级事件的含义。

  • PBT_APMSUSPEND:睡眠前的通知。每个应用大约只有 2 秒的处理宽限期,超过这个时间就有可能被系统强行中断。这里应当把处理内容限定在刷新缓冲区和记录时间这类程度,不要写像通过网络进行收尾处理这样耗时的逻辑。1718
  • PBT_APMRESUMEAUTOMATIC:每次恢复时必定会收到的通知。重新连接、重新调度都应该写在这里。25
  • PBT_APMRESUMESUSPEND:当因用户操作(或用户返回)而恢复时,会在 PBT_APMRESUMEAUTOMATIC 之后收到。由于像远程唤醒这类自动恢复的情况不会收到这个通知,如果只把「恢复处理」写在这里,无人值守恢复时就不会执行重建逻辑26
  • 此外,在电池电量紧张等关键情况下触发的睡眠,根本不会发出事前通知18 因此应该把 Resume 一侧的处理写成幂等的,以确保即便没有收到 Suspend 事件,恢复处理依然能够正常成立。

还有一点和处理恢复同样重要,那就是把缺失区间明确地记录为「缺失」。跨越睡眠的采集数据并不是「没有值」,而是「因为系统停止了,所以根本没有测量」,如果把这段区间连同 suspend/resume 的时刻一起记录在日志和数据中,之后看到图表空白的人就不会把它误认为是故障。关于长期运行应用的日志应该记录哪些内容,「产业用相机长期运行崩溃排查 —— 句柄泄漏篇」一文也有讨论。

另外,与睡眠相似、同样属于「不知不觉就变慢了」这类问题的,还有 Windows 11 效率模式(EcoQoS)带来的限制,可以参考「Windows 效率模式是什么 —— 绿叶图标与关闭方法」一文。

6. 在指定时刻可靠运行 ── 任务计划程序的唤醒执行

像「凌晨 2 点汇总数据并传输」这样以时刻为起点的处理,如果用常驻应用的计时器加上睡眠抑制来实现,并不是好的思路,因为这意味着要整晚扼杀睡眠。这类用途的正解是任务计划程序中的「执行任务时唤醒计算机」(WakeToRun)

启用了 WakeToRun 的任务,会在指定的执行时刻把电脑从睡眠或休眠中唤醒,并保持系统处于唤醒状态,直到任务完成为止(如果系统本来就是唤醒状态,也会要求保持唤醒直到任务完成)。唤醒时屏幕可能仍然是熄灭的,这属于正常现象。19

不过,要让唤醒真正成立是有前提条件的。正如官方故障排查文档中所列出的,前提是电源选项中的「允许使用唤醒计时器」(Allow Wake Timer)已启用、BIOS 一侧的唤醒设置也已启用,而近年来的笔记本电脑出于省电设计的考虑,不允许唤醒的配置也并不少见。20 可以用 powercfg /waketimers 列出当前生效的唤醒计时器,12 因此务必在部署目标的实际设备上验证「是否真的会被唤醒」。如果希望由自家应用来触发唤醒,还有一种手段是把 SetWaitableTimerfResume 参数设为 TRUE,创建一个可唤醒计时器。在这种情况下,系统自动唤醒后只会在无人值守空闲计时器(最短 2 分钟)期间保持唤醒,如果应用没有通过 SetThreadExecutionState 声明「正在使用中」,系统就会很快重新进入睡眠。如果唤醒后的处理耗时较长,正确的组合方式是与第 4 章介绍的抑制手段搭配使用。27

关于任务本身的执行账户、以 0x1 结束的问题、防止多重启动等运维设计,已经整理在「任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计」一文中。

7. 判断表 ── 抑制睡眠、应对恢复,还是唤醒执行

这三种设计并非互斥关系,而是应该按应用类型确定主次,组合使用。

应用类型 首选方案 组合方式・补充说明
测量・数据采集(数小时至数天的连续测量) 仅在测量区间内用 ES_SYSTEM_REQUIRED 抑制睡眠 也务必实现恢复处理(用户操作触发的睡眠无法阻止10)。使用电池供电的 Modern Standby 机型存在 5 分钟强制终止的限制,因此应将 AC 供电作为前提条件13
夜间批处理・定时传输 任务计划程序 + WakeToRun19 比「常驻 + 抑制」更省电、更可靠。前提是需要确认唤醒计时器许可已启用20
常驻监控・通知代理 以睡眠为前提(通过 PowerModeChanged 检测 → 重新连接・重新调度・记录缺失区间)15 如果是以监控为唯一用途的专用设备,应在电源方案一侧而非应用中禁用睡眠
桌面操作工具(演示、仪表盘展示等) 仅在展示期间将 ES_DISPLAY_REQUIREDES_SYSTEM_REQUIRED 一并使用10 展示结束务必清除。若是常态展示终端,应通过电源设置来处理
24/7 的设备控制・产线 PC 通过电源设置直接禁用睡眠本身(由运维保障) 不要把应用的抑制类 API 当作保险手段。可用 powercfg /requests 作为定期巡检手段12

判断的核心有两个轴:「是否存在可以允许停止运行的时间段」(如果有,选任务计划程序;如果没有,选电源设置),以及「运行在谁的 PC 上」(应用越是运行在用户私人设备或共用笔记本上,就越应该偏向恢复处理,而不是抑制睡眠)。

8. 总结

  • 睡眠分为 S3、休眠(S4)、Modern Standby(S0 low power idle)三种,可以用 powercfg /a 判别。即使在 Modern Standby 机型上,桌面应用也会被 DAM 挂起,因此「睡眠期间应用仍在运行」这一前提并不成立。
  • 睡眠期间,线程和计时器都不会推进。从 Windows 8 开始,相对计时器和等待不会计入睡眠时间,而是把剩余时间保留下来,.NET 的计时器也从 .NET 11 开始表现出相同的行为(.NET 10 为止有可能在恢复后立即触发)。经过时间相关的 API 存在计入与不计入睡眠时间两种情况。请用 Stopwatch 管理经过时间,用 DateTime 管理挂钟时间,并在恢复时重新校准基准。
  • TCP 连接会因中间设备的空闲超时而悄悄失效。在恢复事件中先丢弃再重新连接,是稳妥的做法。
  • 抑制睡眠时,应仅在必要区间内使用 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)。它无法阻止用户操作触发的睡眠,在 Modern Standby 机型使用电池供电时还会在 5 分钟后被强制终止。可以用 powercfg /requests 进行确认。
  • 恢复处理应通过 SystemEvents.PowerModeChanged / WM_POWERBROADCAST 实现。由于挂起通知的宽限期只有大约 2 秒,而且也存在没有通知就直接睡眠的情况,应把 Resume 一侧的处理写成幂等的,并记录缺失区间。
  • 定时执行的正解是任务计划程序的 WakeToRun。设计时应连同唤醒计时器的电源设置以及在实际设备上验证唤醒情况一并考虑进去。

相关文章

相关咨询领域

合同会社小村软件承接测量、监控、数据采集等长时间运行的 Windows 应用的设计与实现,也处理「夜里意外停止」「记录出现断档」这类长期运行特有故障的排查工作。涉及睡眠与电源管理、难以复现的症状排查,也欢迎向我们咨询。

参考资料

  1. Microsoft Learn,System Sleeping States。关于 S1~S4 都是不执行计算任务的睡眠状态、S3 仅保持内存而 S4 保存到休眠文件,以及可以用 powercfg /a 列出系统上可用睡眠状态的说明。  2 3 4 5

  2. Microsoft Learn,System power states。关于 S0 low power idle(Modern Standby)下系统会以低功耗方式部分保持运行、支持 Modern Standby 的 SoC 系统不使用 S1~S3,以及关键转换不会发出通知的说明。  2 3

  3. Microsoft Learn,Overview of Modern Standby Testing and Diagnostics。关于可以用 powercfg /a(通过显示 Standby (S0 Low Power Idle))判别是否支持 Modern Standby 的说明。  2

  4. Microsoft Learn,Desktop Activity Moderator。关于 DAM 会把桌面应用的执行抑制到与 S3 等效的程度、交互式会话的进程会被全部线程挂起而会话 0 会被限流、挂起前会发出 WM_POWERBROADCAST 通知、计时器行为与运行时间可能同壁钟时间不一致,以及需要考虑连接生命周期的说明。  2 3 4 5 6 7 8

  5. Microsoft Learn,SetWaitableTimerEx function。关于相对时间指定的计时器,在 Windows 7 及更早版本中会计入低功耗状态的时间(睡眠期间倒计时仍会推进),而从 Windows 8 开始不再计入(睡眠期间倒计时不会推进)的说明。  2

  6. Microsoft Learn,Environment.TickCount made consistent with Windows timeout behavior。关于 Environment.TickCount/TickCount64 在 .NET 10 为止基于 GetTickCount64(计入睡眠时间),而从 .NET 11 开始改为基于 QueryUnbiasedInterruptTime(不计入)的这一破坏性变更、指定超时的等待类 API(SleepEx/WaitForMultipleObjectsEx)自 Windows 8 起就已经不再计入非运行时间,以及这一变更可能导致某些代码在恢复后不再立即触发计时器的说明。  2 3 4 5

  7. Microsoft Learn,Windows Time。关于 GetTickCount/GetTickCount64 的经过时间会计入睡眠与休眠的时间,以及 QueryUnbiasedInterruptTime 仅计入 working state 时间的说明。  2 3

  8. Microsoft Learn,Acquiring high-resolution time stamps。关于受管代码中的 System.Diagnostics.Stopwatch 以 QPC 作为时间基础,以及 QueryPerformanceCounter 返回的计数值会计入 standby、hibernate、connected standby 期间的时间的说明。  2

  9. Microsoft Learn,Load Balancer TCP Reset and Idle Timeout。关于负载均衡器的默认行为是在达到空闲超时(默认 4 分钟)时静默丢弃数据流、发送 TCP 重置是需要显式启用的功能,以及可以用 TCP keep-alive 作为应对措施的说明。  2

  10. Microsoft Learn,SetThreadExecutionState function。关于 ES_CONTINUOUS/ES_SYSTEM_REQUIRED/ES_DISPLAY_REQUIRED 的含义、不带 ES_CONTINUOUS 时只会重置空闲计时器、无法阻止用户触发的睡眠操作,以及仅在必要处理期间设置、完成后清除的使用示例的说明。  2 3 4 5 6 7 8

  11. Microsoft Learn,System Sleep Criteria。关于系统会对调用过 SetThreadExecutionState 的应用/线程进行计数,并在计数为零且没有用户输入时进入睡眠的说明。  2

  12. Microsoft Learn,Powercfg command-line options。关于 /requests 会列出阻止睡眠或屏幕关闭的电源请求、/requestsoverride 可以忽略特定进程的请求,以及 /waketimers 可以列出当前生效的唤醒计时器的说明。  2 3 4 5

  13. Microsoft Learn,PowerSetRequest function。关于 PowerRequestSystemRequired/PowerRequestExecutionRequired 等请求类型、Modern Standby 在 DC 电源下请求会在超过睡眠超时时间的 5 分钟后被终止、因用户操作触发睡眠时请求会结束,以及附加理由字符串并在场景开始前 Set、结束后立即 Clear 的最佳实践的说明。  2 3 4 5

  14. Microsoft Learn,PowerCreateRequest function。关于通过指定 REASON_CONTEXT 创建电源请求对象,并在不再需要时用 CloseHandle 释放的说明。  2

  15. Microsoft Learn,SystemEvents.PowerModeChanged Event。关于这是一个在挂起/恢复时触发的事件、如果消息泵没有运转就不会触发(服务中需要隐藏窗体等)、由于是 static 事件,不取消订阅就会泄漏的说明。  2 3 4

  16. Microsoft Learn,WM_POWERBROADCAST message。关于电源管理事件会以 WM_POWERBROADCAST 消息(PBT_APMSUSPEND/PBT_APMRESUMEAUTOMATIC/PBT_APMRESUMESUSPEND 等)的形式通知给窗口的说明。  2

  17. Microsoft Learn,PBT_APMSUSPEND event。关于该事件会在挂起前发出通知、处理的宽限期大约为 2 秒,超过则可能被系统中断的说明。  2

  18. Microsoft Learn,System Power Management Events。关于因电池电量严重不足等引发的紧急挂起不会发出事前通知,以及挂起通知的处理在每个应用中最多 2 秒就会超时的说明。  2 3

  19. Microsoft Learn,ITaskSettings::get_WakeToRun method。关于 WakeToRun 会在任务执行时唤醒计算机,并保持唤醒状态直到任务完成,以及唤醒时屏幕有时仍会保持熄灭的说明。  2 3

  20. Microsoft Learn,Automatic maintenance。关于当定时唤醒不生效时应确认的项目包括 BIOS 的唤醒设置、电源选项中的「Allow Wake Timer」、任务的 WakeToRun 设置,以及近年来的笔记本电脑普遍采用不允许 S3 唤醒的配置的说明。  2 3

  21. Microsoft Learn,What is Modern Standby。关于 Modern Standby 在 S0 低功耗空闲模型下,在保持网络连接的同时实现瞬间开/关,以及从按下电源按钮到屏幕点亮的恢复时间不到 1 秒的说明。 

  22. Microsoft Learn,QueryUnbiasedInterruptTime function。关于 unbiased interrupt time 只计算 working state 的时间,不计入睡眠与休眠时间的说明。 

  23. Microsoft Learn,Modern standby network connectivity。关于通过 Adaptive Connected Standby,在电池供电时,只要没有需要的场景,睡眠期间的网络活动就会被静止的说明。 

  24. Microsoft Learn,PowerToys Awake utility。关于这是一个无需更改电源方案就能让 PC 保持唤醒状态的实用工具,其运作机制是生成一个请求机器状态的后台线程,退出后会恢复为通常的电源方案行为的说明。 

  25. Microsoft Learn,PBT_APMRESUMEAUTOMATIC event。关于这是每次恢复时必定会分发的事件、并不代表用户存在,以及在检测到用户活动后会随后分发 PBT_APMRESUMESUSPEND 的说明。 

  26. Microsoft Learn,PBT_APMRESUMESUSPEND event。关于在因用户操作或检测到用户而恢复时,会在 PBT_APMRESUMEAUTOMATIC 之后分发,以及在远程唤醒时只会分发 PBT_APMRESUMEAUTOMATIC 的说明。 

  27. Microsoft Learn,System Wake-up Events。关于将 SetWaitableTimer 的 fResume 设为 TRUE 就能用计时器唤醒系统、自动唤醒后会设置最短 2 分钟的无人值守空闲计时器,以及如果不通过 SetThreadExecutionState 表明正在使用中就会重新进入睡眠的说明。 

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

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

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

常见问题

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

如何做到只在应用运行期间不让 PC 进入睡眠?
基本做法是在处理开始时调用 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED),结束时通过 SetThreadExecutionState(ES_CONTINUOUS) 清除。只有在还想让屏幕保持点亮时,才需要额外加上 ES_DISPLAY_REQUIRED。这个方法只对因无操作超时而触发的自动睡眠有效,无法阻止用户按下电源按钮或合上笔记本盖子等明确指示的睡眠。可以用 powercfg /requests 确认抑制是否生效。
什么是 Modern Standby?它与传统睡眠有什么不同?
这是一种也被称为 S0 low power idle 的新型睡眠方式,特点是系统在低功耗状态下仍部分保持运行,并能瞬间恢复。支持 Modern Standby 的机型不再支持传统的 S3 睡眠。不过,「持续运行」仅限于操作系统允许的活动,桌面应用会被 DAM(Desktop Activity Moderator)按线程整体挂起,因此从应用的角度看,其效果与 S3 相同,依然会停止运行。可以用 powercfg /a 确认自己的 PC 属于哪种方式。
为什么从睡眠中恢复后 TCP 连接会无法使用?
因为睡眠期间应用无法进行通信,连接会变为空闲状态,而路径上的 NAT、防火墙、负载均衡器等中间设备会因空闲超时而丢弃该连接的数据流。许多设备在丢弃时并不会发出通知,而是悄悄地断开,因此应用端的套接字表面上看起来仍然正常,直到恢复后第一次收发数据时才会出错,或者一直卡到超时为止。收到恢复事件后,应把连接视为可疑对象并重新建立,这是稳妥的做法。
要让夜间批处理稳定运行,是应该抑制睡眠,还是使用任务计划程序?
首选方案是任务计划程序中的「执行任务时唤醒计算机」(WakeToRun)。它会在指定的执行时刻把 PC 唤醒,并保持运行状态直到任务完成,因此不需要整晚都抑制睡眠。不过,如果电源选项中的唤醒计时器被禁用,PC 就不会被唤醒,所以必须确认「允许使用唤醒计时器」的设置,并在实际设备上验证是否真的能唤醒。而全程抑制睡眠不仅会白白浪费这段时间的电力,还会覆盖用户自己的电源设置,是一种不太得体的设计。

作者简介

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

Go Komura

小村软件有限公司 代表

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

返回博客列表