「監控應用程式應該整晚都在運作,早上一看,圖表卻在凌晨一點就停住了」「桌上型電腦上穩定運作了好幾個月的收集工具,一換成筆記型電腦,記錄就開始出現缺漏」── 在長時間持續執行的業務應用程式諮詢案例中,這是最典型不過的症狀。查看日誌,既沒有例外也沒有當機,只是有好幾個小時份的記錄整段消失。而元兇往往不是應用程式的錯誤,而是 Windows 的電源管理。
麻煩的是,「睡眠」這個詞聽起來只有一種,實際上內容並不只一種。傳統的 S3 睡眠、休眠狀態(S4),以及近年筆記型電腦主流的 Modern Standby(S0 low power idle),從應用程式的角度看到的停止方式不同,對策是否有效也不同。尤其 Modern Standby 常因「睡眠中系統仍在運作」的宣傳而被誤解,但實際上桌面應用程式反而會被積極地停止執行。
本文將先掌握睡眠的種類,以及睡眠期間應用程式行為的最低限度知識,接著整理「不讓系統睡眠」「以睡眠為前提」「在指定時刻喚醒」這三種設計該如何區分使用,並附上實作範例與判斷表。
1. 先講結論
由於結論不少,這裡分成前提(1.1)、「不讓系統睡眠」的設計(1.2)、「配合睡眠」的設計(1.3)三部分列出。建議先讀完 1.1,判斷自己的應用程式屬於 1.2 還是 1.3 之後再往下讀正文,這樣最快(判斷表在第 7 章)。
1.1. 前提 ── 睡眠的種類,以及停止期間發生的事
- Windows 的睡眠有傳統的 S3 睡眠、休眠狀態(S4)、Modern Standby(S0 low power idle)這幾種,支援 Modern Standby 的機種不支援 S1~S3。自己的電腦屬於哪一種,可以用
powercfg /a確認。123 - 即使在 Modern Standby 期間,桌面應用程式也不會持續運作。DAM(Desktop Activity Moderator)會暫停桌面行程的執行緒(工作階段 0 的服務則是被節流)。請不要以「因為是 S0 所以應該還在動」這種前提來設計。4
- 睡眠期間執行緒不會被排程執行,計時器期限的計算方式也因 API 世代而異。Windows 8 以後,相對指定的計時器與等待(
SetWaitableTimer的相對指定、SleepEx等)在睡眠期間不會累計時間,剩餘時間會延續到復原之後。56 .NET 的計時器在 .NET 10 為止的實作會計入睡眠時間(若睡眠期間已到期,會在復原後立即觸發),預定從 .NET 11 起改為不計入(.NET 11 在本文撰寫時尚未發行,請參見 3.1 的附註)。6 經過時間 API 也同樣分成包含睡眠時間(GetTickCount、QueryPerformanceCounter = Stopwatch)與不包含睡眠時間(QueryUnbiasedInterruptTime)兩種。78 - 睡眠期間變成閒置狀態的 TCP 連線,有可能被 NAT、防火牆、負載平衡器等中間裝置的閒置逾時機制悄悄丟棄(例如 Azure Load Balancer 的預設值是 4 分鐘後無通知丟棄)。設計上應以「復原後需要重新連線」為前提。94
1.2. 「不讓系統睡眠」的設計(第 4 章)
- 抑止睡眠的正規做法是
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)。不過它只對因無操作逾時而觸發的自動睡眠有效,無法防止使用者按下電源按鈕或闔上筆電上蓋所指示的睡眠。應只在需要的區段設定,結束後務必清除。1011 - 抑止是否生效可以用
powercfg /requests確認。若使用PowerCreateRequest系列 API,可以在要求上附加理由字串,並顯示在這份清單中,有利於運維時的調查。121314 - 如果像是 24 小時持續運作的設備用電腦,需求本來就是「完全不讓系統睡眠」,正確做法應該是用電源設定停用睡眠,而不是靠應用程式的抑止 API(具體設定位置見 7.1)。
1.3. 「配合睡眠」的設計(第 5 章、第 6 章)
- 若以睡眠為前提來設計,.NET 可用
SystemEvents.PowerModeChanged、Win32 可用WM_POWERBROADCAST(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC)來偵測。暫停通知的緩衝時間,每個應用程式大約只有 2 秒,電池電量緊迫時甚至可能沒有通知就直接進入睡眠。15161718 - 沒有視窗的主控台或服務型應用程式,可以透過
PowerRegisterSuspendResumeNotification在沒有 HWND 的情況下接收相同的通知(最小範例見 5.1)。19 - 若要在固定時刻確實執行,工作排程器的「執行工作時喚醒電腦」(WakeToRun)是第一選擇。它會讓系統一直保持喚醒直到工作完成為止。不過這項功能依賴電源選項中的允許喚醒計時器設定,因此必須在實機上確認確實會喚醒。2021
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。222
這裡的重點是搭載於 Modern Standby 機種上的 DAM(Desktop Activity Moderator)。DAM 是在進入待機時,把桌面應用程式的執行壓制到與 S3 睡眠相同程度的機制,互動工作階段的行程會所有執行緒都被暫停,工作階段 0 的服務則會被節流(大部分時間被暫停,只斷續執行)。4 也就是說,「因為是 Modern Standby,所以睡眠期間應用程式應該還在動」這種期待其實正好相反,從應用程式的角度看,停止方式和 S3 一樣,而且和 S3 不同的是,「系統本身在動,但只有應用程式停下來」,因此會以時間偏差或計時器行為不一致的形式呈現,建議這樣理解會比較保險。4
自己的電腦(或客戶現場的電腦)屬於哪一種方式,不需要管理員權限,用下面的指令就能確認。3
> powercfg /a
以下のスリープ状態がこのシステムで利用可能です:
スタンバイ (S0 低電力アイドル) ネットワークに接続されています
休止状態
...
若顯示 スタンバイ (S3) 就是 S3 機種,顯示 S0 低電力アイドル 就是 Modern Standby 機種。遇到「換成筆記型電腦之後就開始出現記錄缺漏」這類諮詢時,第一步一定會先確認這一點。
3. 睡眠期間、以及復原後,應用程式會發生什麼事
3.1. 執行緒與計時器
睡眠期間(S3/S4,以及 DAM 暫停期間)執行緒不會被執行。14 容易被忽略的是,計時器或等待的「期限」是如何計算睡眠時間的,這一點依 API 的層級而有不同的處理方式。以和下一節經過時間 API 表格相同的形式列出,會是這樣:
| 計時器・等待的種類 | 睡眠期間的處理方式 | 復原後的行為 |
|---|---|---|
Win32 的相對計時器(SetWaitableTimer / SetWaitableTimerEx 的相對指定) |
Windows 8 以後不會累計時間(Windows 7 以前會把低電力狀態的時間也算進去)5 | 等待剩餘時間後才觸發5 |
指定逾時的等待 API(SleepEx / WaitForMultipleObjectsEx 等) |
Windows 8 以後不計入非運作時間6 | 延續剩餘時間繼續等待6 |
.NET 的受控計時器(System.Threading.Timer 等) |
.NET 10 為止會計入(以 GetTickCount64 為基礎),預定從 .NET 11 起不計入(以 QueryUnbiasedInterruptTime 為基礎)6 | .NET 10 為止若已過期限會在復原後立即觸發,預定從 .NET 11 起會等待剩餘時間後才觸發6 |
可喚醒計時器(SetWaitableTimer 的 fResume 設為 TRUE) |
會將系統本身喚醒23 | 喚醒後觸發,但若在無人閒置計時器(最短 2 分鐘)期間沒有宣告「使用中」,會再次進入睡眠(第 6 章)23 |
第三行提到的變更文件,將其說明為 Environment.TickCount64 的底層機制改變所帶來的重大變更(breaking change),文件本身也提醒「有可能出現復原後計時器不再立即觸發的程式碼」。6
關於 .NET 11 的相關敘述
.NET 11 在本文撰寫時(2026 年 7 月)尚未發行。.NET 採用每年 11 月發行主要版本的年度週期,最近一版的 .NET 10 於 2025 年 11 月 11 日發行。24 本文中關於 .NET 11 行為的描述,是根據 Microsoft 公開的重大變更文件(處於預覽階段的資訊)所整理的預定內容,正式發行(GA)後請務必查閱最新文件確認。6
也就是說,假設「用 10 秒間隔的 System.Threading.Timer 進行量測的應用程式睡眠了 8 小時」,8 小時份的觸發無論如何都不會一次全部補上,但復原後究竟是立即觸發一次,還是等待剩餘時間後才觸發,取決於 API 的層級與執行環境(runtime)的版本。不論哪一種,睡眠期間的樣本都會缺失。與其依賴「復原後立即觸發」來設計重建處理,不如在第 5 章的復原事件中明確地重新排程,這樣比較安全。
3.2. 經過時間的量測會產生偏差
經過時間 API 當中,同時混雜著包含睡眠時間與不包含睡眠時間的兩種。
| API | 睡眠時間 |
|---|---|
| GetTickCount / GetTickCount64 | 包含7 |
| QueryPerformanceCounter(= .NET Stopwatch 的底層) | 包含(standby、hibernate、connected standby)8 |
| QueryUnbiasedInterruptTime | 不包含(僅計入 working state 的時間)257 |
| Environment.TickCount / TickCount64 | .NET 10 為止包含(以 GetTickCount64 為基礎),從 .NET 11 起改為不包含(以 QueryUnbiasedInterruptTime 為基礎)6 |
「用 Stopwatch 判斷經過 10 秒就取下一筆樣本」這樣的程式碼,一旦跨過睡眠,就會變成「經過了 8 小時又 10 秒」;反過來說,以 TickCount 為基礎的經過時間判斷,在 .NET 11 之後行為也會改變。把「處理花費的時間」交給 Stopwatch,「下一次應該執行的時鐘時刻」用 DateTime / DateTimeOffset 管理,並在復原事件(第 5 章)時重新取得基準,做好這樣的職責分工,就不會被睡眠或執行環境更新牽著走。短週期計時器本身的設計,在「Windows 上為什麼應先用事件等待而不是計時器等待 - 避免以約 15.6ms 粒度做輪詢」中有討論。
3.3. TCP 連線會「悄悄地」失效
睡眠期間應用程式無法通訊,連線因此變成閒置狀態。問題出在路徑上的裝置。NAT、防火牆、負載平衡器等中間裝置會以逾時機制丟棄閒置的流量,但在許多組態下,丟棄時並不會通知連線兩端,而是悄悄地丟棄。舉例來說,Azure Load Balancer 的預設行為是「達到閒置逾時(預設 4 分鐘)的流量會被靜默丟棄」。9 公司內部的路由器,以及裝置端內建的 TCP 堆疊,也都有類似的逾時機制。
結果就是,復原後應用程式的 socket 表面上看起來仍然正常,卻會在下一次傳送時發生錯誤,或是在等待回應時卡住直到逾時為止。DAM 的文件也明確指出,需要考慮行程暫停對連線存續時間與交握所造成的影響。4 此外,Modern Standby 機種在使用電池供電時,預設會讓睡眠期間的網路活動整個靜止下來(Adaptive Connected Standby)。26 接收到復原事件後,應懷疑連線已經失效,將其捨棄並重新連線,這是標準做法。關於連線「看起來活著、實際上已經失效」這類問題的排查,也可以參考「TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分」。
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 行程異常結束的話,抑止效果也會一併消失,所以「抑止之後忘記解除、導致電腦永遠不睡眠」這種擔心,只要重新啟動就能解決,但反過來說,這也不能作為對抗當機的保障手段。 - 正如其名稱所示,這個函式設定的是呼叫端執行緒的執行狀態。10 設定與解除請在同一個執行緒中進行。由於
async/await的接續(continuation)有可能在不同的執行緒集區執行緒上執行,若在夾著await的實作中呼叫Begin()與End(),解除動作就會在與設定睡眠抑止時不同的執行緒上落空,導致原本的執行緒存活期間,抑止效果會一直殘留下去。安全的做法是固定呼叫在身分保證一致的執行緒上(例如 UI 執行緒),若設計上需要跨執行緒呼叫,請改用稍後提到的、以控制代碼為基礎的電源要求 API。
自 Windows 7 起,還有另一組目的相同、較新的 API,也就是電源要求(PowerCreateRequest / PowerSetRequest / PowerClearRequest)。這組 API 在建立要求時,可以透過 REASON_CONTEXT 傳入理由字串,在實務上是一大優點;要求種類除了 PowerRequestSystemRequired 之外,還有能夠抑止 Modern Standby 機種上行程暫停的 PowerRequestExecutionRequired。1413 官方的最佳實務是「在情境開始前立刻 Set,結束後立刻 Clear,行程結束前清理控制代碼」。13
不過 Modern Standby 機種有一項重要限制。在電池供電的 Modern Standby 系統中,SystemRequired/ExecutionRequired 要求會在超過睡眠逾時後的 5 分鐘被強制終止。此外不論電源方式為何,只要因使用者操作(電源按鈕、闔上上蓋、開始功能表的睡眠選項)而進入睡眠,要求就會結束。13 也就是說,「在筆記型電腦上,即使闔上上蓋、即使用電池供電也要持續運作」是無法只靠應用程式的努力來實現的。這類需求應該由電源設定與運維方式(設定闔蓋不睡眠、以交流電供電)來保障。
抑止是否真的有生效,可以在具有系統管理員權限的命令提示字元中確認。
> powercfg /requests
SYSTEM:
[PROCESS] \Device\HarddiskVolume3\Apps\SensorLogger.exe
powercfg /requests 是列出正在阻止睡眠、關閉螢幕的電源要求的指令,既可用來調查「不知為何不會睡眠的電腦」,也能用來確認自己應用程式的抑止是否生效。12 輸出的解讀方式如下。
- 輸出依要求種類分成不同標題區塊。基本上是
DISPLAY(抑止關閉螢幕)、SYSTEM(抑止睡眠)、AWAYMODE,此外也會出現對應PowerRequestExecutionRequired的EXECUTION分類。若在上面範例中,自己的應用程式出現在SYSTEM:底下,就代表已經成功抑止了系統睡眠。1213 - 標題底下每一行開頭的
[PROCESS]/[SERVICE]/[DRIVER],代表要求來源的種類。這個種類與名稱,可以直接當作傳給powercfg /requestsoverride的引數。12 - 沒有對應要求的種類,會顯示「沒有要求」的說明行(英文環境下為
None.)。確認自己應用程式的抑止時,只需要看SYSTEM底下是否出現自己的行程即可,這樣就足夠了。 - 如果使用
PowerCreateRequest系列 API 附加理由字串,該字串就會顯示在這份清單中,運維方也能因此知道「這是為了哪項處理而抑止睡眠」。1413
反過來說,也請留意管理員可以透過 powercfg /requestsoverride,設定為忽略特定行程的要求。12 設計上的前提是,抑止 API 終究只是一種「請求」,而非絕對保證。
最後談談分寸的問題。若實作成應用程式啟動期間全程持續設定 ES_CONTINUOUS | ES_SYSTEM_REQUIRED,就等於常駐應用程式永久性地扼殺了使用者所設定的電源方案。若是筆記型電腦會耗損電池,若是共用電腦則會影響其他用途。原則上應該把抑止範圍限定在「實際正在執行、不能被睡眠打斷的處理區段」(官方文件的範例也是「開始錄影時設定、錄影完成時清除」10)。另外,使用者端也有一個暫時的迴避方案,就是 PowerToys Awake,它內部同樣是以相同機制(要求執行狀態的執行緒)運作。27 這也可以用來劃分「該由應用程式實作抑止,還是交給運維工具處理」的界線。
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:
// 緩衝時間很短:只做 flush 緩衝區、記錄量測停止時間這兩件事
_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;
}
}
官方文件明確指出這個事件有兩個注意事項:若訊息幫浦(message pump)沒有在運轉就不會觸發(Windows 服務需要準備隱藏表單之類的東西),以及因為是 static 事件,若疏忽取消訂閱就會造成洩漏。15 GUI 應用程式可以直接使用,但主控台/服務型的收集應用程式,則需要自行準備接收 Win32 WM_POWERBROADCAST 的訊息視窗,或是使用不需要 HWND 就能接收回呼的 PowerRegisterSuspendResumeNotification(DAM 環境下的通知也是走這條路徑)。416
5.1. 在沒有視窗的應用程式中接收復原通知
對於像收集工具或 Windows 服務這類沒有視窗的應用程式來說,與其自行製作訊息視窗,使用 PowerRegisterSuspendResumeNotification 所需的工夫更少。這個函式的用法是在 Flags 指定 DEVICE_NOTIFY_CALLBACK,Recipient 則傳入指向 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS(內含回呼函式與內容)的指標,成功時傳回 ERROR_SUCCESS(0)。Windows 8 / Windows Server 2012 以後可以使用。1928
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
// 在沒有視窗的主控台/服務中接收暫停・復原通知的最小範例
// 不需要訊息幫浦。Windows 8 / Windows Server 2012 以降可用
internal sealed class SuspendResumeNotifier : IDisposable
{
private const uint DEVICE_NOTIFY_CALLBACK = 2; // powrprof.h
private const uint PBT_APMSUSPEND = 0x0004; // winuser.h
private const uint PBT_APMRESUMEAUTOMATIC = 0x0012; // winuser.h
private const uint ERROR_SUCCESS = 0;
// ULONG DeviceNotifyCallbackRoutine(PVOID Context, ULONG Type, PVOID Setting)
private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);
[StructLayout(LayoutKind.Sequential)]
private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
{
public IntPtr Callback;
public IntPtr Context;
}
[DllImport("powrprof.dll")]
private static extern uint PowerRegisterSuspendResumeNotification(
uint flags,
ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient,
out IntPtr registrationHandle);
[DllImport("powrprof.dll")]
private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);
// 委派(delegate)要保存在欄位中(若只放在區域變數,會被 GC 回收,通知時就會當機)
private readonly DeviceNotifyCallbackRoutine _callback;
private IntPtr _registration;
// 解除失敗的註冊會殘留在 OS 端。只有這個委派需要保留到行程結束為止
// 的存放處(參見下方的 Dispose)
private static readonly List<DeviceNotifyCallbackRoutine> AbandonedCallbacks = new();
public SuspendResumeNotifier()
{
_callback = OnPowerNotification;
var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
{
Callback = Marshal.GetFunctionPointerForDelegate(_callback),
Context = IntPtr.Zero,
};
uint result = PowerRegisterSuspendResumeNotification(
DEVICE_NOTIFY_CALLBACK, ref parameters, out _registration);
if (result != ERROR_SUCCESS)
{
throw new InvalidOperationException(
$"PowerRegisterSuspendResumeNotification failed with {result}.");
}
}
private uint OnPowerNotification(IntPtr context, uint type, IntPtr setting)
{
// 這個方法是由 OS(原生端)直接呼叫的。若讓 managed 例外
// 從這裡拋到外面,因為 managed 端沒有承接的呼叫端,
// 就會連整個行程一起崩潰。像是日誌寫入目的地已滿、collector
// 已經被釋放這類「有可能發生」的失敗,會導致應用程式每次睡眠
// 都當機,所以要在內部完整處理,並且務必回傳代碼
try
{
switch (type)
{
case PBT_APMSUSPEND:
// 緩衝時間大約 2 秒。只做緩衝區的 flush 與停止時間的記錄
_logger.Info("suspend at {0:O}", DateTimeOffset.Now);
_collector.Pause();
break;
case PBT_APMRESUMEAUTOMATIC:
// 即使是無人值守的復原也一定會收到。較重的重建處理要移到回呼之外執行
_logger.Info("resume at {0:O}", DateTimeOffset.Now);
_recovery.RequestRebuild();
break;
}
}
catch (Exception ex)
{
// 有可能連日誌輸出本身都失敗,這裡也要做防護
try { _logger.Error("power notification failed: {0}", ex); }
catch { /* 如果在這裡重新拋出,上面的 try 就失去意義了 */ }
}
return ERROR_SUCCESS; // 回傳 Windows 的錯誤代碼
}
public void Dispose()
{
if (_registration == IntPtr.Zero)
{
return;
}
uint result = PowerUnregisterSuspendResumeNotification(_registration);
if (result != ERROR_SUCCESS)
{
// 若在尚未成功解除的情況下捨棄控制代碼,登記會繼續留在 OS 端,
// 卻失去了追蹤手段。若此時這個實例被回收,_callback 也會
// 隨之消失,OS 就會呼叫已經釋放的函式指標。下一次暫停時
// 就會連整個行程一起崩潰,所以只把委派保留下來
lock (AbandonedCallbacks)
{
AbandonedCallbacks.Add(_callback);
}
_logger.Error("PowerUnregisterSuspendResumeNotification failed with {0}.", result);
return; // 既然尚未成功解除,就不捨棄控制代碼
}
_registration = IntPtr.Zero;
}
}
實作上要注意的重點有三個。
- 不要讓例外拋出回呼函式之外。這個方法是由 OS 直接呼叫的,一旦 managed 例外跨越邊界,因為 managed 端沒有承接的呼叫端,會連整個行程一起崩潰。像是日誌輸出失敗、存取已經釋放的物件,這類實際可能發生的失敗,會導致應用程式每次睡眠都當機。請用
try/catch把整個內容包起來,並且務必回傳代碼。 - 回呼是由系統端的執行緒呼叫的。
PBT_APMSUSPEND的緩衝時間約 2 秒,這一點和透過視窗接收時相同,因此不能在這裡執行重新連線或跨網路的善後處理。1718 復原處理也一樣,設定旗標交給另一個執行緒處理會比較安全。 - 不要忘記取消註冊。要把註冊控制代碼傳給
PowerUnregisterSuspendResumeNotification。29 也請檢查它的傳回值。若在尚未成功解除的情況下捨棄註冊控制代碼,OS 端會殘留註冊,卻失去追蹤手段。若在這種狀態下實例被回收,回呼的委派也會隨之消失,OS 就會呼叫已經釋放的函式指標。 - 若想改用視窗控制代碼來接收,也可以把
DEVICE_NOTIFY_WINDOW_HANDLE傳給 user32.dll 的RegisterSuspendResumeNotification(這種方式會照常收到WM_POWERBROADCAST)。30
若要做成 Windows 服務,也可以把 ServiceBase 的 CanHandlePowerEvent 設為 true,並覆寫 OnPowerEvent(PowerBroadcastStatus)。因為相同的電源事件會透過服務控制管理員傳遞過來,所以可以不用 P/Invoke 就寫出來。31
5.2. Win32 層級事件的意義
不論是透過視窗,還是透過回呼接收,事件的意義都是共通的。這裡要先掌握清楚。
- PBT_APMSUSPEND:睡眠前一刻的通知。每個應用程式大約只有 2 秒的緩衝時間,一旦超過就可能被系統中斷。這裡只做 flush 與時間記錄這類最小限度的事,不可以撰寫像跨網路善後這種耗時的處理。1718
- PBT_APMRESUMEAUTOMATIC:每次復原都一定會收到的通知。重新連線、重新排程都要寫在這裡。32
- PBT_APMRESUMESUSPEND:因使用者操作(或使用者回來)而復原時,會在 PBT_APMRESUMEAUTOMATIC 之後收到。由於遠端喚醒等自動復原不會收到這個通知,若只把「復原處理」寫在這裡,無人值守復原時就不會執行重建。33
- 此外,在電池電量緊迫等臨界情況下進入睡眠時,連事前通知本身都不會送達。18 即使沒有收到 Suspend 事件,也要讓復原處理仍然成立,因此 Resume 端要寫成冪等(idempotent)的。
5.3. 把缺測記錄為缺測
另一件和復原對應同樣重要的事,是把缺測記錄為缺測。跨越睡眠期間的收集資料並非「沒有值」,而是「因為系統當時停止運作,所以沒有量測」,只要把該區間連同 suspend/resume 的時間一起留在日誌與資料兩邊,之後看到圖表出現空白的人就不會把它誤認為是故障。關於長時間運轉的應用程式,日誌應該留下哪些內容,「工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計」也有討論。
另外,與睡眠類似、屬於「不知不覺間變慢了」這類問題的,還有 Windows 11 效率模式(EcoQoS)造成的抑制。這部分請參考「Windows 的效率模式是什麼 - Windows 11 的綠色葉子圖示代表什麼,以及如何關閉」。
6. 在固定時刻確實執行 ── 工作排程器的喚醒執行
像「凌晨兩點彙總並傳送」這種以時間點為起點的處理,若用常駐應用程式的計時器加上睡眠抑止來實現,並不是好做法,因為這樣會整晚扼殺系統睡眠。這種用途的正解是工作排程器的「執行工作時喚醒電腦」(WakeToRun)。
啟用 WakeToRun 的工作,會在執行時刻把電腦從睡眠或休眠中喚醒,並讓系統一直保持喚醒直到工作完成為止(若電腦原本就已經是喚醒狀態,也會要求維持喚醒直到工作完成)。喚醒時螢幕有可能保持關閉,這是正常現象。20
不過,要成功喚醒是有條件的。正如官方疑難排解文件所列,前提是電源選項中的「允許喚醒計時器」(Allow Wake Timer)必須啟用,且 BIOS 端的喚醒設定也必須啟用,而近年來筆記型電腦基於省電設計,不允許喚醒的組態並不罕見。21 用 powercfg /waketimers 可以列出目前有效的喚醒計時器12,因此務必在導入現場的實機上驗證「是否真的會喚醒」。若想用自己的應用程式來喚醒,也可以使用把 SetWaitableTimer 的 fResume 設為 TRUE 的可喚醒計時器這種手段。這種情況下,系統在自動喚醒後,只會在無人閒置計時器(最短 2 分鐘)期間保持喚醒,若應用程式沒有透過 SetThreadExecutionState 宣告「使用中」,就會很快再次進入睡眠。若喚醒後的處理時間較長,正確的搭配方式是與第 4 章的抑止機制組合使用。23
關於工作本身的執行帳戶、以 0x1 結束的問題、防止多重啟動等運維設計,整理在「工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計」。
7. 判斷表 ── 抑止、復原對應,還是喚醒
這三種設計並非互斥,而是要依應用程式的種類決定主從關係並加以組合。
| 應用程式種類 | 第一候選 | 組合・補充說明 |
|---|---|---|
| 量測・資料收集(數小時~數天的連續量測) | 只在量測區段用 ES_SYSTEM_REQUIRED 抑止 |
也務必實作復原對應(無法防止使用者操作造成的睡眠10)。電池供電的 Modern Standby 機種有 5 分鐘強制終止的限制,因此要把交流電供電列為前提條件13 |
| 夜間批次・定時傳輸 | 工作排程器 + WakeToRun20 | 比常駐 + 抑止更省電、更可靠。前提是要確認允許喚醒計時器已啟用21 |
| 常駐監控・通知代理程式 | 以睡眠為前提(用 PowerModeChanged 偵測 → 重新連線・重新排程・記錄缺測)15 | 若是以監控為主要目的的專用機,應該在電源方案端停用睡眠,而不是靠應用程式 |
| 桌面操作工具(簡報、儀表板顯示等) | 只在顯示期間把 ES_DISPLAY_REQUIRED 與 ES_SYSTEM_REQUIRED 併用10 |
顯示結束務必清除。若是常時顯示的終端機,改用電源設定處理 |
| 24/7 的設備控制・產線電腦 | 用電源設定停用睡眠本身(以運維方式保障) | 不要把應用程式的抑止 API 當成保險。可以用 powercfg /requests 做定期檢查12 |
判斷的軸線有兩個:「是否有可以停止運作的時段」(有的話用工作排程器,沒有的話用電源設定),以及「在誰的電腦上執行」(越是在使用者私人裝置、共用筆電上執行的應用程式,越應該傾向復原對應,而非抑止)。
7.1. 「用電源設定停用睡眠本身」的具體內容
表格中多次出現的「用電源設定停用」,具體來說指的是以下操作。像設備控制電腦這種不靠應用程式、而靠運維方式來阻止睡眠的情況,必須做到這個程度才算真正得到保障。
| 要做的事 | 位置・指令 |
|---|---|
| 把進入睡眠的時間設為「無」 | 設定 > 系統 > 電源(筆記型電腦則是「電源與電池」) > 螢幕與睡眠。或是 控制台 > 硬體和音效 > 電源選項 > 變更計畫設定。用指令則是 powercfg /change standby-timeout-ac 0(0 代表不進入睡眠)1234 |
| 也一併檢視關閉螢幕的時間 | 同一畫面中的「關閉螢幕」。用指令則是 powercfg /change monitor-timeout-ac 012 |
| 變更闔蓋・電源按鈕的動作 | 電源選項 > 選擇按下電源按鈕時的行為 > 選擇「不執行任何動作」。這條路徑是 SetThreadExecutionState 無法防止的10 |
| 確認機種的方式 | powercfg /a。是 S3 機種還是 Modern Standby 機種,後續要注意的重點會不同3 |
Modern Standby 機種還需要多一層注意。支援 Modern Standby 的機種,會在螢幕關閉時就進入待命。官方文件列出的觸發時機,除了電源按鈕、闔上上蓋、開始功能表選擇睡眠之外,還包括「系統閒置逾時」。35 也就是說,若還停留在 S3 機種的感覺,以為「只要關掉睡眠的逾時就沒問題」,就會留下一條從螢幕關閉進入待命的路徑。請重新檢視睡眠與螢幕關閉兩邊的逾時設定,用 powercfg /a 確認機種方式,並且務必把「在實機上放置一整晚,確認不會停止運作」納入操作流程之中。
電池供電時的處理方式也很容易被忽略。standby-timeout-ac 是交流電供電時的設定,電池供電時使用的是 standby-timeout-dc。若設備用電腦是筆記型電腦,安全的做法是明確把交流電供電列為前提條件(如第 4 章所述,電池供電的 Modern Standby 機種,電源要求本身會在 5 分鐘後被強制終止)。13
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。設計時請連同喚醒計時器的電源設定、在實機上確認確實喚醒都一併納入考量。
相關文章
- 工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計
- Windows 上為什麼應先用事件等待而不是計時器等待 - 避免以約 15.6ms 粒度做輪詢
- TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分
- 工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計
- Windows 的效率模式是什麼 - Windows 11 的綠色葉子圖示代表什麼,以及如何關閉
相關諮詢領域
合同會社小村軟體承接量測、監控、資料收集等長時間執行的 Windows 應用程式的設計與實作,也承接「半夜停止運轉」「記錄出現缺漏」等長期執行特有問題的調查。若遇到與睡眠、電源管理相關、難以重現的症狀排查,也歡迎諮詢。
參考連結
-
Microsoft Learn, System Sleeping States。關於 S1~S4 屬於睡眠狀態且不執行任何計算工作、S3 只保留記憶體、S4 則寫入休眠檔,以及 powercfg /a 可以列出可用睡眠狀態的說明。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, System power states。關於 S0 low power idle(Modern Standby)會讓系統以低電力局部持續運作、支援 Modern Standby 的 SoC 系統不使用 S1~S3,以及臨界狀態轉換不會發出通知的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, Overview of Modern Standby Testing and Diagnostics。關於可用 powercfg /a(顯示 Standby (S0 Low Power Idle))判別是否支援 Modern Standby 的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, Desktop Activity Moderator。關於 DAM 會把桌面應用程式的執行壓制到與 S3 相同程度、互動工作階段的行程會所有執行緒都被暫停・工作階段 0 則被節流、暫停前會有 WM_POWERBROADCAST 通知,以及需要考慮計時器行為、運作時間與時鐘時刻的不一致、連線存續時間的說明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, SetWaitableTimerEx function。關於相對時間指定的計時器,在 Windows 7 以前會把低電力狀態的時間也算進去(睡眠期間倒數也會前進),而 Windows 8 以後則不包含(睡眠期間倒數不會前進)的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, Environment.TickCount made consistent with Windows timeout behavior。關於 Environment.TickCount / TickCount64 在 .NET 10 為止以 GetTickCount64 為基礎(包含睡眠時間),從 .NET 11 起改為以 QueryUnbiasedInterruptTime 為基礎(不包含)這項重大變更、Windows 8 以後指定逾時的等待 API(SleepEx / WaitForMultipleObjectsEx)已經不再計入非運作時間,以及這項變更可能導致復原後計時器不再立即觸發的程式碼的說明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Windows Time。關於 GetTickCount / GetTickCount64 的經過時間包含睡眠、休眠時間,QueryUnbiasedInterruptTime 只包含 working state 時間的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, Acquiring high-resolution time stamps。關於受控程式碼中的 System.Diagnostics.Stopwatch 以 QPC 為時間基礎、QueryPerformanceCounter 傳回的刻度數會包含 standby、hibernate、connected standby 期間的說明。 ↩ ↩2
-
Microsoft Learn, Load Balancer TCP Reset and Idle Timeout。關於負載平衡器的預設行為是達到閒置逾時(預設 4 分鐘)時靜默丟棄流量、傳送 TCP 重設需要另外明確啟用,以及對策上會使用 TCP keep-alive 的說明。 ↩ ↩2
-
Microsoft Learn, SetThreadExecutionState function。關於 ES_CONTINUOUS / ES_SYSTEM_REQUIRED / ES_DISPLAY_REQUIRED 的意義、不加 ES_CONTINUOUS 只會重設閒置計時器、無法防止使用者的睡眠操作,以及只在必要處理期間設定、完成後清除的使用範例說明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, System Sleep Criteria。關於系統會計算呼叫過 SetThreadExecutionState 的應用程式/執行緒數量,計數歸零且沒有使用者輸入時就會進入睡眠的說明。 ↩ ↩2
-
Microsoft Learn, Powercfg command-line options。關於 /requests 會列出阻礙睡眠或關閉螢幕的電源要求、/requestsoverride 可以忽略特定行程的要求、/waketimers 可以列出有效喚醒計時器的說明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, PowerSetRequest function。關於 PowerRequestSystemRequired / PowerRequestExecutionRequired 等要求種類、電池供電的 Modern Standby 系統中要求會在超過睡眠逾時後的 5 分鐘被強制終止、因使用者操作進入睡眠時要求會結束,以及附上理由字串、在情境開始前 Set・結束後立刻 Clear 的最佳實務說明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, PowerCreateRequest function。關於指定 REASON_CONTEXT 建立電源要求物件、不再需要時以 CloseHandle 釋放的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, SystemEvents.PowerModeChanged Event。關於這是在暫停/恢復時觸發的事件、若訊息幫浦沒有運轉就不會觸發(服務需要準備隱藏表單等)、因為是 static 事件所以不取消訂閱就會造成洩漏的說明。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WM_POWERBROADCAST message。關於電源管理事件會以 WM_POWERBROADCAST 訊息(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC / PBT_APMRESUMESUSPEND 等)通知視窗的說明。 ↩ ↩2
-
Microsoft Learn, PBT_APMSUSPEND event。關於這是在暫停前一刻收到的通知、處理的緩衝時間約為 2 秒、超過時可能被系統中斷的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, System Power Management Events。關於因電池電量臨界等原因觸發的緊急暫停不會有事前通知、暫停通知的處理每個應用程式最長逾時 2 秒的說明。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, PowerRegisterSuspendResumeNotification function。關於在 Flags 指定 DEVICE_NOTIFY_CALLBACK、Recipient 傳入指向 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS 的指標、回呼的 Type 會帶入 PBT_APMSUSPEND / PBT_APMRESUMESUSPEND / PBT_APMRESUMEAUTOMATIC、成功時傳回 ERROR_SUCCESS,以及 Windows 8 / Windows Server 2012 以後包含在 Powrprof.dll 中的說明。 ↩ ↩2
-
Microsoft Learn, ITaskSettings::get_WakeToRun method。關於 WakeToRun 會在工作執行時喚醒電腦、並讓系統保持喚醒直到工作完成,以及喚醒時螢幕可能保持關閉的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, Automatic maintenance。關於定時喚醒無法運作時的確認項目,包括 BIOS 的喚醒設定、電源選項的「Allow Wake Timer」、工作的 WakeToRun 設定,以及近年筆記型電腦普遍不允許 S3 喚醒的說明。 ↩ ↩2 ↩3
-
Microsoft Learn, What is Modern Standby。關於 Modern Standby 是 S0 低電力閒置模型、在維持網路連線的情況下實現瞬時開關,以及復原(從按下電源按鈕到螢幕點亮)不到 1 秒的說明。 ↩
-
Microsoft Learn, System Wake-up Events。關於將 SetWaitableTimer 的 fResume 設為 TRUE 可用計時器喚醒系統、自動喚醒後會設定最短 2 分鐘的無人閒置計時器,若沒有透過 SetThreadExecutionState 表示使用中就會再次進入睡眠的說明。 ↩ ↩2 ↩3
-
Microsoft, .NET and .NET Core Support Policy。關於 .NET 主要版本每年 11 月發行、.NET 10 是於 2025 年 11 月 11 日發行的 LTS 版本的說明。 ↩
-
Microsoft Learn, QueryUnbiasedInterruptTime function。關於 unbiased interrupt time 只計入 working state 的時間、不包含睡眠、休眠時間的說明。 ↩
-
Microsoft Learn, Modern standby network connectivity。關於透過 Adaptive Connected Standby,在電池供電時,除非有需要的情境,否則睡眠期間的網路活動會被靜止的說明。 ↩
-
Microsoft Learn, PowerToys Awake utility。關於這是不需要變更電源方案就能讓電腦保持喚醒的公用程式,其運作方式是產生要求機器狀態的背景執行緒,結束後就會恢復一般電源方案行為的說明。 ↩
-
Microsoft Learn, DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS structure。關於此結構具有 Callback 與 Context 兩個成員、回呼函式 DeviceNotifyCallbackRoutine 的形式為 ULONG(PVOID Context, ULONG Type, PVOID Setting) 並回傳 Windows 錯誤代碼的說明。 ↩
-
Microsoft Learn, PowerUnregisterSuspendResumeNotification function。關於傳入註冊控制代碼以解除通知註冊、成功時傳回 ERROR_SUCCESS 的說明。 ↩
-
Microsoft Learn, RegisterSuspendResumeNotification function。關於在 Flags 指定 DEVICE_NOTIFY_WINDOW_HANDLE 會將事件傳遞給視窗、指定 DEVICE_NOTIFY_CALLBACK 則可透過回呼接收的說明。 ↩
-
Microsoft Learn, ServiceBase.OnPowerEvent(PowerBroadcastStatus) Method。關於 Windows 服務用來接收電腦電源狀態變化的方法,以及必須在 CanHandlePowerEvent 屬性為 true 時才需要覆寫的前提說明。 ↩
-
Microsoft Learn, PBT_APMRESUMEAUTOMATIC event。關於這是每次復原都一定會送達的事件、並不代表使用者存在,若偵測到使用者活動,之後會再送達 PBT_APMRESUMESUSPEND 的說明。 ↩
-
Microsoft Learn, PBT_APMRESUMESUSPEND event。關於因使用者操作、偵測到使用者而復原時,會在 PBT_APMRESUMEAUTOMATIC 之後送達,而遠端喚醒時只會送達 PBT_APMRESUMEAUTOMATIC 的說明。 ↩
-
Microsoft Learn, Sleep idle timeout。關於這是指定自動進入睡眠前的無操作時間的設定、最小值 0 代表「不進入睡眠」的說明。 ↩
-
Microsoft Learn, Prepare software for modern standby。關於進入 Modern Standby 的時機是螢幕關閉時,觸發原因包括電源按鈕、闔上上蓋、從設定選擇睡眠、系統閒置逾時,DAM 階段之後桌面應用程式的執行會被停止、工作階段 0 的服務會被節流,以及電源要求在交流電供電下可無限期、在電池供電下最長可延長此階段 5 分鐘的說明。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
網路磁碟機與 UNC 路徑的陷阱 ── 業務應用程式處理檔案伺服器(共用資料夾)的實務
整理業務應用程式對共用資料夾進行輸出・監控時常見的麻煩。說明磁碟機代號(Z:)為何無法從服務中看到、各執行帳戶所需的權限、錯誤 1219,以及 FileSystemWatcher 的注意事項。
Windows 應用程式的工作列通知區常駐與 Toast 通知 —— NotifyIcon 的陷阱與 AppNotification 的選型
本文整理將業務用 Windows 應用程式常駐於工作列通知區,並透過 Toast 通知告知使用者的實作要點。內容涵蓋 NotifyIcon 的正確用法與「關閉後仍留在通知區」的設計、檔案總管重新啟動時的重新註冊、三種 Toast API(Windows App SDK Ap...
自行開發的 Windows 應用程式被當成病毒處理時 ── Microsoft Defender 誤判的因應方式,以及與效能影響的相處之道
整理自行開發的 Windows 應用程式被 Microsoft Defender 誤判時的正規因應方式。從現代防毒機制的原理、向 Microsoft 回報誤判、從隔離還原,到排除設定的正確做法與風險,一併解說。
Arm 版 Windows 能執行業務應用程式嗎 ── x64 模擬(Prism)與原生 DLL・COM 的現實
本文為開發者・資訊系統部門解答「Arm 版 Windows 能執行業務應用程式嗎」這個問題,整理 x64 模擬(Prism)的運作原理、驅動程式等無法執行的層級、.NET 的 AnyCPU 與 P/Invoke 問題,以及 Arm 對應檢查清單。
MAX_PATH 與 Windows 路徑・檔案名稱的陷阱 ── 260 字元限制、保留名稱、結尾句點、大小寫
本文整理「找不到檔案」這類問題的常見原因──路徑與檔案名稱的限制。內容涵蓋 MAX_PATH=260 字元的組成、透過 LongPathsEnabled 啟用長路徑、CON 等保留名稱、結尾句點的正規化,直到 Path.Combine 的陷阱。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- 只想在應用程式執行期間讓電腦不進入睡眠,該怎麼做?
- 基本做法是在處理開始時呼叫 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED),結束時再用 SetThreadExecutionState(ES_CONTINUOUS) 清除。只有在也想讓螢幕保持點亮時,才追加 ES_DISPLAY_REQUIRED。它只對因無操作逾時而觸發的自動睡眠有效,無法防止使用者按下電源按鈕或闔上上蓋所指示的睡眠。可以用 powercfg /requests 確認抑止是否生效。
- 什麼是 Modern Standby?和傳統的睡眠有什麼不同?
- 這是又稱為 S0 low power idle 的新式睡眠方式,特徵是系統雖然以低電力運作,卻仍會局部持續動作,並可瞬間復原。支援 Modern Standby 的機種不支援傳統的 S3 睡眠。不過「持續動作」的僅限於 OS 允許的活動,桌面應用程式會被 DAM(Desktop Activity Moderator)連同執行緒一起暫停,因此從應用程式的角度來看,停止方式和 S3 相同。自己的電腦屬於哪一種方式,可以用 powercfg /a 確認。
- 為什麼睡眠復原後 TCP 連線就不能用了?
- 因為睡眠期間應用程式無法通訊,連線會變成閒置狀態,路徑上的 NAT、防火牆、負載平衡器等中間裝置會因閒置逾時而丟棄該流量。許多裝置在丟棄時並不會發出通知,而是悄悄地丟掉,所以應用程式端的 socket 表面上看起來仍然正常,要到復原後第一次收發時才會出現錯誤,或是卡住直到逾時為止。標準做法是收到復原事件後,就懷疑連線已經失效並重新建立。
- 要確實執行夜間批次,該用睡眠抑止還是工作排程器比較好?
- 工作排程器的「執行工作時喚醒電腦」(WakeToRun)是第一選擇。它會在執行時刻喚醒電腦,並保持喚醒直到工作完成,因此不需要整晚扼殺睡眠功能。不過如果電源選項中的喚醒計時器被停用,就不會喚醒,因此「允許喚醒計時器」的設定與在實機上確認確實喚醒是必要的步驟。若全程持續抑止睡眠,不僅會浪費這段期間的電力,也會變成覆寫使用者電源設定的失禮設計。