动态

详情 返回 返回

如何正確使用SetThreadExecutionState來阻止Windows進入睡眠 - 动态 详情

最近產品有個需求,需要在升級的時候阻止Windows系統進入自動睡眠。需求到手後,小搜了一下,搜到SetThreadExecutionState這個函數,相關的博客挺多,官方文檔也挺清晰,想必應該是手拿把掐了,結果沒想到連續踩了好幾個坑。現在,我就把SetThreadExecutionState的基本使用方法和我踩過的坑整理出來分享給大家。

函數原型

EXECUTION_STATE SetThreadExecutionState(
  [in] EXECUTION_STATE esFlags
);

上面就是SetThreadExecutionState的函數原型,在Win32Api裏面算是非常簡單的函數原型了。入參和返回值都是EXECUTION_STATE類型,入參表示要設置的狀態,返回值表示設置前的原有狀態,如果失敗返回就是0。

img

EXECUTION_STATE可以是上面這些值或者它們的組合值。從上表可以看出ES_USER_PRESENT不可用,ES_CONTINUOUS用來表示此次設置的狀態是否保持有效,所以實際能夠設置的狀態有三種,下面簡單介紹一下每一種的作用。

ES_SYSTEM_REQUIRED (0x00000001)

ES_SYSTEM_REQUIRED的作用是重置系統空閒計時器。系統空閒計時器,或者叫睡眠空閒超時,就是在持續一定時間沒有用户輸入時,使系統自動進入睡眠或新式待機狀態。在Windows11的"系統-電源和電池-使我的設備在以下時間後進入睡眠狀態"中可以設置超時時間。

舉個例子,假如我設置了在1分鐘後使系統進入睡眠,然後我在運行以下代碼後就不再對系統進入任何操作

await Task.Delay(30*1000);
SetThreadExecutionState(ES_SYSTEM_REQUIRED);

那麼,原本系統會在1分鐘後進入睡眠,但是代碼在30秒的時候重置了系統空閒計時器,所以實際上系統會在1分30秒後進入睡眠。

如果和ES_CONTINUOUS一起使用,那麼會永遠阻止系統進入睡眠

SetThreadExecutionState(ES_SYSTEM_REQUIRED | ES_CONTINUOUS);

取消的方法是,再調用一次SetThreadExecutionState,設置ES_CONTINUOUS並且不設置ES_SYSTEM_REQUIRED,即

SetThreadExecutionState(ES_CONTINUOUS);

ES_DISPLAY_REQUIRED (0x00000002)

ES_DISPLAY_REQUIRED的作用是重置顯示空閒計時器。顯示空閒計時器,或者叫顯示空閒超時,對應的是Windows11的"系統-電源和電池-在此時間後關閉我的屏幕"設置。ES_SYSTEM_REQUIRED的使用方式和ES_SYSTEM_REQUIRED相同,不再舉例。

那麼有一個問題,假如我把睡眠空閒超時和顯示空閒超時都設置為1分鐘,顯然,在系統空閒1分鐘後,屏幕會關閉,系統也會進入睡眠。

如果我的應用程序在30秒的時候,僅重置了睡眠空閒超時,那麼1分鐘後屏幕會關閉,但系統會繼續運行,到1分30秒的時候進入睡眠。

如果我的應用程序在30秒的時候,僅重置了顯示空閒超時會怎麼樣呢?我在Win11 24H2在做了驗證,結果是在1分鐘的時候,屏幕沒有關閉,系統也沒有進入睡眠,在1分30秒時,屏幕關閉和系統睡眠同時發生。可見,在Win11 24H2中,不存在一種系統睡眠而屏幕未關閉的狀態,系統需要關閉屏幕後才能進入睡眠。

ES_AWAYMODE_REQUIRED (0x00000040)

使用ES_SYSTEM_REQUIRED可以防止系統自動進入睡眠,那如果用户手動點擊睡眠,或者其他應用程序調用API使系統進入睡眠,有沒有辦法阻止這一行為呢?

我們知道,當我們點擊開始菜單的睡眠按鈕時,屏幕會關閉,系統會進入睡眠,應用程序會被掛起,例如,使用下面的代碼,每隔1秒鐘,打印一次當前時間

while(true)
{
    Console.WriteLine($"{DateTime.Now:T}");
    await Task.Delay(1000);
}

在代碼運行中,點擊系統開始菜單的睡眠,過一會兒再喚醒系統,可以看到,代碼的輸出結果為:

//......
12:39:24
12:39:25
12:39:36
12:39:37
//......

可以看到中間有10幾秒的間隔沒有輸出,這説明系統睡眠時代碼被掛起了,系統喚醒後代碼才繼續執行。

要想我們的代碼能夠在系統睡眠時在後台繼續執行,可以使用ES_AWAYMODE_REQUIRED。如果我們把代碼改成這樣,然後執行相同的操作,那麼我們仍然會得到時間連續的輸出,因為我們的代碼沒有被掛起,而是在後台繼續執行。

SetThreadExecutionState(ES_AWAYMODE_REQUIRED | ES_CONTINUOUS);
while(true)
{
    Console.WriteLine($"{DateTime.Now:T}");
    await Task.Delay(1000);
}

ES_AWAYMODE_REQUIRED和ES_SYSTEM_REQUIRED的區別是,ES_AWAYMODE_REQUIRED不會阻止系統進入睡眠,但系統也沒有完全睡眠,因為我們的代碼仍在運行。在這個狀態中,其他沒有設置ES_AWAYMODE_REQUIRED的應用會被系統掛起。

小節

  • ES_SYSTEM_REQUIRED用來阻住自動睡眠,ES_DISPLAY_REQUIRED用來阻止屏幕自動關閉,ES_AWAYMODE_REQUIRED使應用程序在睡眠時能夠在後台執行。
  • 不帶ES_CONTINUOUS是單次請求,重置一次超時計時;帶上ES_CONTINUOUS是持續請求,單獨設置ES_CONTINUOUS可取消持續請求。
  • ES_AWAYMODE_REQUIRED只適用持續請求。

在多線程中的使用

雖然站在用户的角度來看,是應用程序阻止了睡眠,但SetThreadExecutionState這個函數,如它的函數名稱一樣,它設置的是線程的執行請求(電源請求)。

進程的電源請求狀態,是所有線程的電源請求狀態的並集。下面的代碼在三個線程中分別設置了ES_DISPLAY_REQUIRED、ES_SYSTEM_REQUIRED和ES_AWAYMODE_REQUIRED三種持續的電源請求:

var starter1 = new ThreadStart(()=>
{
    SetThreadExecutionState(EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
    Thread.Sleep(30000);
});

var starter2 = new ThreadStart(()=>
{
    SetThreadExecutionState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
    Thread.Sleep(30000);
});

var starter3 = new ThreadStart(()=>
{
    SetThreadExecutionState(EXECUTION_STATE.ES_AWAYMODE_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
    Thread.Sleep(30000);
});

var t1 = new Thread(starter1);
t1.Start();

var t2 = new Thread(starter2);
t2.Start();

var t3 = new Thread(starter3);
t3.Start();

運行後,我們在控制枱使用powercfg /requests命令來查詢當前設備上應用程序的電源請求,結果如下:
img

結果是AwakeDemo-Console.exe這個進程同時具有三種電源請求。

在一個線程中設置的電源請求,只能在該線程中取消,或者等待線程終止後,電源請求自動取消。

服務進程中無法設置ES_DISPLAY_REQUIRED

話不多説,直接上示例演示:

首先用VS的Windows服務模板新建一個項目

img

然後在Service1.cs中添加以下代碼,使服務在啓動時同時設置DISPLAY、SYSTEM和AWAYMODE請求

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

[FlagsAttribute]
public enum EXECUTION_STATE : uint
{
    ES_AWAYMODE_REQUIRED = 0x00000040,
    ES_CONTINUOUS = 0x80000000,
    ES_DISPLAY_REQUIRED = 0x00000002,
    ES_SYSTEM_REQUIRED = 0x00000001
}

protected override void OnStart(string[] args)
{
    SetThreadExecutionState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_AWAYMODE_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
}

編譯後找到輸出的exe文件,在cmd中輸入以下命令創建和運行服務

//創建一個名為awake-demo的服務,二進制文件是指定的exe文件的路徑
sc create awake-demo binPath= "D:\Projects\CSharp Projects\AwakeDemo\AwakeDemo\bin\Release\AwakeDemo.exe"

//啓動awake-demo服務
sc start awake-demo

然後用powercfg /requests查詢,結果如下:
img

可以看到,服務進程只設置了SYSTEM和AWAYMODE請求。通過簡單驗證可以發現,確實只有SYSTEM和AWAYMODE請求生效了。

Tips

本文展示的結果都來自於支持新式待機(Modern Standby)的移動設備,在不支持新式待機的傳統設備上,表現可能會有差異。

Add a new 评论

Some HTML is okay.