從睡眠恢復就壞掉的應用程式 ── Windows 電源事件的機制,以及耐得住恢復的業務應用寫法

· · Windows, 電源管理, Windows 開發, 業務應用程式, 裝置控制, 故障調查, Win32 API

「闔上筆電,隔天早上打開,業務應用程式滿是錯誤。」「設備監控應用程式只有午休之後才掉資料。」「匯出到 Excel 的常駐工具,偶爾會因連線錯誤停住。」── 這些單據共用同一個嫌犯。睡眠

桌上型電腦當主流那個時代的業務應用程式,是在「PC 一直開著」這個沒說出口的假設上寫的。如今主戰場是筆電,預設閒置幾分鐘就會睡。在支援 Modern Standby 的機器上,睡眠的語意本身已和傳統模型不同。本文以在 Windows 上撰寫業務應用程式與設備控制軟體的開發者為對象,依一次資訊整理:睡眠前後作業系統會通知應用程式什麼、什麼會壞掉,以及怎樣寫才能耐得住恢復。

1. 先講結論

  • 睡眠是應用程式「沒有拒絕權」的事件。事前會以 WM_POWERBROADCAST(PBT_APMSUSPEND)通知,但寬限期大約 2 秒,緊急暫停時連通知都不會來。12
  • 從暫停恢復時會收到 PBT_APMRESUMEAUTOMATIC;使用者發起的恢復還會再收到 PBT_APMRESUMESUSPEND。重連這類必要工作原則上放在前者。不過進出 Modern Standby 低耗電閒置,並不總是對齊這些通知,因此把通知當成輔助。34
  • 設計時假設 TCP 連線、序列埠與裝置控制代碼跨過恢復活不下來。在恢復通知或通訊錯誤時重建它們的重連邏輯,才是主戲。
  • 注意計時器與時間的處理。週期工作在睡眠期間會停,恢復後立刻怎麼觸發,依計時器 API 與執行環境而異。「經過時間大幅跳躍」也會發生,安全做法是恢復時重建排程。
  • 不想被睡眠打斷的區間,要明確抑制睡眠。SetThreadExecutionState(ES_SYSTEM_REQUIRED)或電源要求(PowerSetRequest),工作結束後務必清除。45
  • Modern Standby 機器在睡眠期間系統仍會間歇執行,但桌面應用程式會被暫停。不能抱著「我們的應用程式睡眠期間也該繼續跑」的期待。6
  • 標準調查工具是 powercfg(/requests、/lastwake、/sleepstudy)與事件記錄裡的 Kernel-Power。

2. 睡眠前後發生什麼 ── 電源事件的流程

作業系統把電源狀態變化以 WM_POWERBROADCAST 訊息廣播給每個應用程式。2 涉及睡眠與恢復的主要事件有三個。

事件 意義
PBT_APMSUSPEND 即將進入睡眠(最後的準備機會)
PBT_APMRESUMEAUTOMATIC 已恢復(恢復時一定會到)
PBT_APMRESUMESUSPEND 使用者操作造成的恢復(這個是有條件的)

PBT_APMSUSPEND 是睡眠前一刻的通知,此時可以關閉檔案、儲存狀態來準備。不過有兩個條件。第一,每個應用程式允許的處理時間大約 2 秒,超時系統就不再等待而繼續。1 第二,電池電量危急這類緊急暫停時,沒有事前通知就立刻睡2 「必須在睡眠前做完」的設計不成立。把通知當成「趕得上就做」的機會,主體放在恢復側。

恢復側是兩階段。從暫停轉換恢復時會收到 PBT_APMRESUMEAUTOMATIC。在此之上,若機器是因電源按鈕或按鍵等使用者操作而恢復(或之後偵測到使用者在場),接著會收到 PBT_APMRESUMESUSPEND。反過來說,因網路遠端喚醒或維護而無人值守恢復,只會送到 PBT_APMRESUMEAUTOMATIC。3 這兩階段本身就是工作怎麼拆的提示──重建連線這類機械性恢復放在 PBT_APMRESUMEAUTOMATIC,畫面更新或重新登入提示這類面向使用者的動作放在 PBT_APMRESUMESUSPEND

睡眠與恢復的通知流程睡眠前一刻會收到約 2 秒寬限的 PBT_APMSUSPEND;恢復時一定會收到 PBT_APMRESUMEAUTOMATIC,只有使用者發起的恢復才會接著收到 PBT_APMRESUMESUSPEND應用程式OS應用程式OS睡眠(程式碼不執行)PBT_APMSUSPEND(約 2 秒寬限)儲存狀態並關閉連線PBT_APMRESUMEAUTOMATIC(恢復時會到)重連並還原狀態PBT_APMRESUMESUSPEND(僅使用者發起的恢復)畫面更新等面向使用者的工作

圖 1: 通知只是「事前一句話,恢復後一句或兩句」。恢復的主角是恢復側的工作。

一般睡眠與緊急暫停的差異一般睡眠會在事前送到約 2 秒準備時間的 PBT_APMSUSPEND,但電池危急這類緊急暫停沒有事前通知就停止,因此依賴事前通知的設計不成立一般睡眠PBT_APMSUSPEND(約 2 秒寬限)準備後停止緊急暫停(電池電量危急)沒有事前通知就停止假設通知一定會來的設計不成立

圖 2: 緊急暫停沒有預警。所以準備是「趕得上就賺到」,主體放在恢復側。

注意 WM_POWERBROADCAST 不區分低耗電狀態的種類(睡眠與休眠)。4 對應用程式正確的抽象是把它當成同一類事件:「停了,又回來了」。沒有視窗的服務與主控台應用程式,可用回呼形式(DEVICE_NOTIFY_CALLBACK)的 RegisterSuspendResumeNotification 收到相同通知。7

把工作拆到恢復的兩階段重連這類機械性恢復放在恢復時會到的 PBT_APMRESUMEAUTOMATIC;畫面更新或重新登入提示這類面向使用者的工作放在只有使用者發起恢復才會到的 PBT_APMRESUMESUSPENDPBT_APMRESUMEAUTOMATIC(恢復時)機械性恢復PBT_APMRESUMESUSPEND(使用者發起的恢復)面向使用者的工作重連並重新開啟控制代碼畫面更新與重新登入提示

圖 3: 無人值守恢復不會送到後者,把必要恢復放在後者就會漏掉。

3. Modern Standby ── 「睡眠」的意義已經變了

另一個必須納入的現代事實是 Modern Standby。傳統 S3 睡眠是「整個系統停下來」的單純模型;Modern Standby 機器上的睡眠,是螢幕熄滅後系統仍間歇執行的智慧型手機式模型。

對業務應用程式重要的是:進入睡眠的第一階段,桌面應用程式會被 Desktop Activity Moderator(DAM)暫停6 系統本身仍不時跑一下以維持網路、接收通知,但受惠的是參與這套機制的元件──一般桌面應用程式的程式碼不會跑。因此從開發者角度看,Modern Standby 與 S3 的結論相同──設計時假設睡眠期間你的程式碼不執行

傳統睡眠與 Modern Standby 的差異傳統 S3 睡眠讓整個系統停止;Modern Standby 下螢幕熄滅後系統仍間歇執行。兩種情況桌面應用程式都會被 DAM 暫停,因此應用程式的程式碼不執行傳統 S3 睡眠:整個系統停止應用程式的程式碼不執行Modern Standby:系統間歇執行桌面應用程式被 DAM 暫停

圖 4: 模型變了,但對桌面應用程式結論相同:「睡眠期間跑不了」。

另一個要注意的是通知有多不可靠。在 Modern Standby 下,進出低耗電閒置並不對齊傳統暫停轉換,連線可能已經斷了卻從未收到通知。把恢復通知當成輔助,把第 5 章依錯誤偵測觸發的重連放在主要恢復路徑上。

另一個差異是行為的「滑溜」感。進入睡眠深處是分階段的,斷線與停止的時機不像 S3 那樣乾脆。「只是螢幕熄了」和「已經睡了」對使用者也很難分辨,因此接到症狀時要確認「有沒有闔上蓋子」以及「閒置了幾分鐘」。

4. 什麼會壞 ── 經典症狀

TCP 連線已經死了。睡眠期間,對方、NAT 與防火牆把你的沉默當成逾時並丟棄連線。更糟的是這一側的通訊端還不知道錯誤,所以要等到恢復後傳送或接收才會失敗。再糟一點,接收等待根本不會出錯(這就是為什麼需要 keepalive)。資料庫連線與 WebSocket 是同一種形狀。

序列埠與 USB 裝置控制代碼失效。USB 連接的裝置在恢復時,看起來可能像「拔掉再插回去」一次,原本開著的控制代碼開始傳回錯誤。這就是設備控制應用程式「只有午休之後才通訊錯誤」的典型模式。重連設計也寫在序列通訊文章裡。

時間的連續性斷了。「每 10 秒輪詢一次」這類由計時器驅動的工作,睡眠期間不會觸發。恢復後立刻怎麼觸發(到期的工作立刻發一次、什麼都不做等到下一個週期,等等)依你用的計時器 API 與執行環境而異,因此不要把錯過的節拍交給隱含行為──安全做法是在恢復通知上重建排程。此外,經過時間計算(與前一個時間戳的差)會突然變成「8 小時份」,平均計算或逾時判斷就會壞掉。「每天凌晨 2 點執行」這類排程工作,若當時 PC 在睡就根本不會跑(需要的話用工作排程器的從睡眠喚醒功能叫醒它)。

時間連續性斷掉的三種形狀週期工作在睡眠期間停止,恢復後觸發依 API 而異,因此恢復時重建排程;與前一個時間戳的差在恢復後變得巨大,因此要防護;排程工作在機器睡著時不會跑,因此考慮工作排程器的從睡眠喚醒週期工作:停止重建排程經過時間:爆開防護異常差值排程:根本沒跑從睡眠喚醒

圖 5: 計時器與時間處理要假設「時間會跳」。三種形狀各有對應的對策類型。

跨過睡眠會壞掉的三件事跨過睡眠,TCP 連線已被對方逾時丟棄,USB 裝置的控制代碼因重連而失效,依經過時間的工作會看到巨大的時間跳躍。分別用重連、重新開啟與差值防護來恢復睡眠區間TCP:對方已丟棄USB 或經過時間?USB:控制代碼無效經過時間:一次跳躍偵測+重連重新開啟裝置防護異常差值

圖 6: 會壞的東西落在三個家族──「連線」、「控制代碼」與「時間連續性」──各自有既定的恢復類型。

對共享資源重新驗證。網路磁碟機與 VPN 恢復後往往需要重新建立,恢復後立刻有一段數秒到數十秒的「啟動谷底」,存取會失敗。比較安全的做法不是恢復後立刻一次重試全部,而是稍等再分階段重試。

5. 寫出耐得住恢復的應用程式

原則只有一件。假設「連線與控制代碼跨過睡眠活不下來」,把應用程式結構化成隨時能恢復。

偵測恢復並恢復。頂層視窗的 WM_POWERBROADCAST 收到 PBT_APMRESUMEAUTOMATIC 時,丟棄持有的連線並重建。重點是不要只依賴恢復通知。漏掉的通知、以及通知到來之前就發生的通訊都是真實的,因此一定要搭配「偵測到通訊錯誤就重連」這條路徑,把恢復通知當成只是讓那條路提早開始的觸發。

// C#: funnel both the resume notification and communication errors into the same reconnect path
protected override void WndProc(ref Message m)
{
    const int WM_POWERBROADCAST = 0x0218;
    const int PBT_APMRESUMEAUTOMATIC = 0x0012;
    if (m.Msg == WM_POWERBROADCAST && (int)m.WParam == PBT_APMRESUMEAUTOMATIC)
    {
        _connectionManager.RequestReconnect();   // idempotent reconnect request
    }
    base.WndProc(ref m);
}

重連工作本身要做成冪等(呼叫幾次都安全),失敗時用指數退避重試,穩態時用 keepalive 及早偵測死連線──把這三件當成一套,你就不只耐得住從睡眠恢復,也耐得住短暫斷網或裝置重開。

耐得住恢復的重連設計恢復通知、通訊錯誤與 keepalive 失敗都匯入同一條冪等重連工作,失敗時以指數退避重試恢復通知(PBT_APMRESUMEAUTOMATIC)冪等重連工作通訊錯誤偵測Keepalive 失敗成功?回到正常運作指數退避後重試

圖 7: 把重連集中到單一冪等路徑,從恢復通知、錯誤偵測或 keepalive 走同一條路。

重新檢視時間的處理。對「距上次經過多久」的工作,偵測到異常大的差值就讓該區間失效(不要折進平均、不要當成逾時)。跨過恢復量測經過時間,必須區分睡眠期間仍前進的時鐘(牆上時間)與實際花在工作上的時間。

不想被睡眠打斷的區間,要明確抑制睡眠。資料遷移、與裝置持續通訊這類絕不能被睡過去的工作期間,可用 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) 讓系統保持清醒(也想讓螢幕保持亮就加上 ES_DISPLAY_REQUIRED)。45 更規矩的做法是電源要求 API(PowerCreateRequest + PowerSetRequest),能附上原因字串,之後 powercfg /requests 就會顯示「誰在擋、為什麼」。8 注意經由 SetThreadExecutionState 的抑制是每個執行緒的,要從設定它的同一條執行緒清除。async/await 這類會換執行緒的工作,改用由控制代碼管理的電源要求側。有幾點要注意。第一,這些抑制的是閒置自動睡眠。擋不住闔上蓋子或從開始功能表選睡眠這類明確的使用者動作,因此即使正在抑制也不能跳過本章的重連設計。第二,Modern Standby 機器用電池時,睡眠逾時過一段時間後這些電源要求也會被切斷。不能中斷的工作必須用交流電源或營運來保證。8 第三,工作結束後務必清除。漏清會變成新 bug:「這台 PC 不知為何睡不著」。

抑制睡眠的兩種手段無論用方便的 SetThreadExecutionState,還是能附原因字串、管理員可用 powercfg 看到的電源要求 API,工作結束後都務必清除絕不能被睡過去的工作區間SetThreadExecutionState電源要求(PowerSetRequest)方便──只要旗標帶原因──powercfg 看得到工作結束後務必清除

圖 8: 兩種手段都把「做完就清掉」當絕對條件。能讓原因可見的電源要求對營運比較友善。

服務與無視窗應用程式RegisterSuspendResumeNotification(DEVICE_NOTIFY_CALLBACK)收回呼通知。7 若持續運作是真正需求,根本解法是重新檢視「把工作常駐在會睡的用戶端 PC」這個設計,改放到伺服器側或不睡眠營運的機器上。

6. 調查 ── powercfg 與事件記錄

電源周邊的調查,作業系統內建的工具就很好用。

  • 睡不著powercfg /requests 列出已發出電源要求的處理程序與驅動程式。「應用程式忘了清 SetThreadExecutionState」也會出現在這裡。
  • 自己醒來powercfg /lastwake 顯示最近一次喚醒原因,powercfg /waketimers 顯示目前預約要喚醒機器的計時器。
  • Modern Standby 品質powercfg /sleepstudy 產生每個睡眠區間的耗電與活動報告。9
  • 確認時間軸:事件記錄(系統)裡的 Kernel-Power 來源保存進入睡眠與恢復的紀錄。拿來對應用程式的記錄,就能客觀確認「錯誤前一刻是否剛恢復」。
電源問題症狀對應到調查指令睡不著的症狀用 powercfg /requests 找出誰握著電源要求;自己醒來的症狀用 /lastwake 與 /waketimers 找出喚醒原因;時間軸用事件記錄裡的 Kernel-Power睡不著powercfg /requests自己醒來powercfg /lastwake 與 /waketimers想確認時間軸事件記錄裡的 Kernel-Power忘了清的睡眠抑制也會出現

圖 9: 症狀對應到調查指令分成三個家族。先確認「剛才有沒有睡」,再拆。

處理單據時,先問「前一刻 PC 有沒有在睡(有沒有闔上蓋子)」,隔離速度會快很多。

7. 總結

  • 睡眠不能拒絕。事前通知(PBT_APMSUSPEND)是大約 2 秒寬限的盡力而為,緊急時不會來。把主要設計放在恢復側。
  • 恢復通知是 PBT_APMRESUMEAUTOMATIC(從暫停恢復時)+ PBT_APMRESUMESUSPEND(使用者操作時)。把依錯誤驅動的重連留在主路徑上,以應付通知沒來的情況。
  • 假設連線與控制代碼跨過恢復活不下來,實作冪等重連+指數退避+keepalive 這三件一套。
  • 依經過時間的工作要加上「異常差值」防護。排程工作要假設睡眠期間不會跑。
  • 絕不能被睡過去的區間,用 SetThreadExecutionState 或電源要求明確抑制睡眠,做完務必清除。
  • 調查用 powercfg(/requests、/lastwake、/sleepstudy)與 Kernel-Power 事件記錄。處理單據時先問「前一刻有沒有睡」。

從應用程式的角度看,睡眠是「時間毫無預警地跳躍、與周圍的連線被切斷,然後又回來」的事件。你有沒有把它織進設計,當成日常生活的一部分而不是異常狀況,決定了筆電時代業務應用程式的穩定度。

相關文章

相關諮詢領域

小村軟體有限公司承接「從睡眠恢復後通訊就斷」、「午休之後與裝置的連線就掉」這類錯誤的根本原因調查、為既有應用程式補上重連邏輯與電源事件處理,以及假設以筆電運作的業務應用程式與設備控制軟體的設計審查。

參考連結

  1. Microsoft Learn, PBT_APMSUSPEND event. 關於這是電腦即將進入暫停狀態前一刻送到的事件;關於應用程式應完成儲存資料所需的工作;以及系統允許約 2 秒處理此通知,繼續超過的應用程式可能被中斷。  2

  2. Microsoft Learn, System Power Management Events. 關於系統會事先廣播睡眠等運作模式變更;關於 PBT_APMSUSPEND 在閒置睡眠前通知以便關閉檔案、儲存資料來準備;關於緊急暫停(電池危急等)沒有事前通知;關於處理此訊息每個應用程式最多約 2 秒、逾時後會被切斷;以及恢復時每個應用程式都會收到通知。  2 3

  3. Microsoft Learn, PBT_APMRESUMESUSPEND event. 關於使用者發起的恢復、或之後偵測到使用者輸入時,會在 PBT_APMRESUMEAUTOMATIC 之後送到此事件;關於遠端喚醒等外部原因造成的恢復只送 PBT_APMRESUMEAUTOMATIC;以及應用程式應重新開啟睡眠時關閉的檔案並準備接受使用者輸入。  2

  4. Microsoft Learn, WM_POWERBROADCAST message. 關於恢復時一定會送 PBT_APMRESUMEAUTOMATIC,使用者輸入造成的恢復還會再送 PBT_APMRESUMESUSPEND;關於此訊息不區分低耗電狀態的種類;關於電源狀態轉換細節記在系統事件記錄;以及呼叫 SetThreadExecutionState 可防止系統進入低耗電狀態。  2 3 4

  5. Microsoft Learn, SetThreadExecutionState function (winbase.h). 關於 ES_SYSTEM_REQUIRED 與 ES_DISPLAY_REQUIRED 能抑制系統的閒置睡眠與顯示器關電;以及用 ES_CONTINUOUS 宣告持續抑制,做完後只呼叫 ES_CONTINUOUS 來清除。  2

  6. Microsoft Learn, Prepare software for modern standby. 關於 Desktop Activity Moderator(DAM)在進入 Modern Standby 轉換的第一階段暫停桌面應用程式;以及系統隨後分階段進入低耗電階段與韌性階段,只有獲准的元件間歇執行。  2

  7. Microsoft Learn, RegisterSuspendResumeNotification function (winuser.h). 關於這是註冊以接收暫停/恢復通知的 API,以及指定 DEVICE_NOTIFY_CALLBACK 後,無視窗應用程式或服務除了對視窗控制代碼的訊息傳遞外,也能以回呼收到通知。  2

  8. Microsoft Learn, PowerSetRequest function (winbase.h). 關於能對以 PowerCreateRequest 建立的電源要求物件設定系統或顯示器保持清醒等要求類型;關於能附上診斷用原因字串;以及未完成的電源要求可用 powercfg /requests 列舉。  2

  9. Microsoft Learn, Modern standby SleepStudy. 關於 powercfg /sleepstudy 產生的報告能依每個 Modern Standby 區間檢視耗電、活動與喚醒原因(電源按鈕、使用者輸入、喚醒計時器等)。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

應用程式能事先得知睡眠並拒絕嗎?
在現行 Windows 上可以收到通知,但不能拒絕。睡眠前一刻會以 WM_POWERBROADCAST 訊息送出 PBT_APMSUSPEND 事件,此時可以關閉檔案、儲存狀態來準備,但每個應用程式允許的處理時間大約只有 2 秒,超時系統就不再等待而繼續。電池電量危急這類緊急暫停時,事前通知本身也不會來。因此「必須在睡眠前做完」的設計不成立;需要的是無論何時被切斷,恢復時都能重建的設計。真的不想被睡眠打斷的工作區間,要用 SetThreadExecutionState 或電源要求(PowerSetRequest)明確抑制睡眠。
要怎麼偵測機器已經恢復?
應用程式有視窗就處理 WM_POWERBROADCAST。從暫停恢復時會收到 PBT_APMRESUMEAUTOMATIC;若恢復是使用者操作(電源按鈕或按鍵)造成的,接著還會收到 PBT_APMRESUMESUSPEND。無人值守恢復後立刻再睡,只會送到 PBT_APMRESUMEAUTOMATIC,因此基本分法是:重連這類必要工作放在 PBT_APMRESUMEAUTOMATIC 側,畫面更新這類面向使用者的工作放在 PBT_APMRESUMESUSPEND 側。沒有視窗的服務與主控台應用程式,可用 RegisterSuspendResumeNotification 搭配 DEVICE_NOTIFY_CALLBACK,以回呼收到相同通知。
睡眠期間能讓應用程式繼續跑嗎?
原則上不能。睡眠期間 CPU 執行本身會停(Modern Standby 機器上,桌面應用程式會被 Desktop Activity Moderator 暫停),應用程式的程式碼不會跑。選擇有兩個。一是只在工作進行中抑制睡眠。用 SetThreadExecutionState 指定 ES_SYSTEM_REQUIRED,或用 PowerCreateRequest/PowerSetRequest 發出電源要求,就能在該區間抑制閒置自動睡眠(可用 powercfg /requests 確認)。這仍擋不住使用者闔上蓋子這類明確的睡眠動作,因此即使正在抑制,仍要為恢復做準備。二是接受睡眠,設計成「恢復後再追上」。夜間批次這類排程工作,也可以用工作排程器的「喚醒電腦以執行這個工作」把 PC 叫醒。真正需要持續跑的工作,應放在伺服器或不睡眠的服務上。
為什麼恢復後 TCP 連線和序列埠就不能用了?
因為網路介面卡和 USB 裝置在睡眠期間也會進入低耗電狀態。TCP 連線早已被對方或 NAT、防火牆逾時丟棄,恢復後的傳送/接收會傳回錯誤(常常要等到出錯才發現)。USB 轉序列埠這類裝置,恢復時有時會被當成裝置拔除再插入,原本開著的控制代碼就失效。兩者都應假設「控制代碼和連線跨過恢復活不下來」,正解是在恢復通知或通訊錯誤時重建連線的重連邏輯。週期性 keepalive 加上失敗時指數退避重試,是既定做法。
要怎麼調查非預期的睡眠或非預期的恢復?
第一個工具是 powercfg 指令。「睡不著」方向用 powercfg /requests 列出哪些處理程序和驅動程式發出了擋住睡眠的電源要求。「自己醒來」方向用 powercfg /lastwake 看最近一次喚醒原因,用 powercfg /waketimers 看目前預約要喚醒機器的計時器。Modern Standby 機器上,powercfg /sleepstudy 會產出睡眠期間耗電與活動的報告。睡眠與恢復的歷史也會記在事件記錄(系統記錄的 Kernel-Power 來源),因此能依時間軸確認「何時睡了、何時因何醒來」。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽