C# 學習筆記:多執行緒 (3) - 優先順序

摘要:多執行緒筆記之三,這次整理的主題是執行緒的優先順序。

溫馨提醒:本文新版本的內容已經整理自電子書《.NET 本事-非同步程式設計》。點我查看出版訊息

Windows 作業系統把執行緒的優先順序分成 32 個等級,編號從最低的 0 至最高的 31,優先權愈高,愈能分到更多 CPU 時間。進一步說,當 Windows 要決定把 CPU 分給誰的時候,會先看看目前有沒有優先等級 31 的執行緒正在等候安排 CPU,若有,就會把 CPU 分給它一段時間。等它跑完配給的時間後,系統會把 CPU 分給其他同樣是優先等級 31 的執行緒。如果目前已經沒有優先等級 31 的執行緒在等待分配資源,才會輪到優先等級 30 的執行緒,然後是 29、28….依此類推。
零頁執行緒

Windows 作業系統啟動時會建立一條特殊的執行緒,叫做「零頁執行緒」(zero page thread)。這條執行緒的的優先等級是 0,而且整個系統當中也就只有它的優先等級是 0。換言之,應用程式的執行緒優先等級不可能為 0。
偷來的圖
想像一群嗷嗷待哺的雛鳥,個個伸長了脖子張大了口等鳥媽媽餵食,但鳥媽卻偏愛其中一隻,只管餵牠。這樣下去,除非那隻受到特別關愛的雛鳥吃飽了,否則其他兄弟姊妹就只有捱餓的份。在 Windows 系統中,低優先等級的執行緒也會發生同樣的狀況,也叫做捱餓(starvation)。多 CPU 的機器能夠減少執行緒捱餓的機會,因為不同優先等級的執行緒可以同時分配給不同的 CPU。

還有一種狀況:有個優先等級 15 的執行緒幸運分配到一段 CPU 時間,可是才執行到一半,就出現另一個更高優先等級的執行緒;此時系統會立刻暫停較低優先的執行緒,並將 CPU 分配給較高優先的執行緒。用鳥版本來說就是:有隻雛鳥幸運分到食物,才剛咬幾口還沒吞下,就被鳥媽硬生生奪回,拿去餵另一隻雛鳥了。

之所以說「幸運分配到」,是因為我們無法精確指定或得知某執行緒究竟何時分配到 CPU,以及分配到多久的時間──這些完全由 Windows 作業系統來控制。我們能控制的,是藉由調整執行緒的優先等級來提高(或降低)執行緒獲得 CPU 資源的機會。

可是,優先等級共 32 級(若零頁執行緒專用的等級 0 不算則為 31 級),該如何決定哪些執行緒要用等級 2、5、12、還是 31 呢?為了簡化此問題,微軟用兩個條件的組合來決定執行緒的優先等級:處理序的優先順序類別(priority class),以及執行緒的優先順序。

處理序的優先順序類別

處理序的優先順序類別有以下六種:
  • 即時(RealTime)
  • 高(High)
  • 高於標準(Above Normal)
  • 標準(Normal)
  • 低於標準(Below Normal)
  • 閒置(Idle)

預設的處理序優先順序是「標準」。應用程式應該只在真有必要時才用「高」優先類別,例如非關 I/O、執行時間短的處理序。至於「即時」優先類別則更應盡量避免,因為它的優先權極高,高到會影響作業系統的正常運作,例如干擾磁碟讀寫或網路傳輸,以及延遲鍵盤與滑鼠輸入的反應(使用者可能會以為系統當掉了)。總之,若無正當理由,別輕易調高應用程式的優先順序。

.NET Framework 的 System.Diagnostics.ProcessPriorityClass 列舉型別定義了處理序的優先順序。以下程式片段示範如何將目前處理序的優先順序類別設定為「高」:

var p = System.Diagnostics.Process.GetCurrentProcess();
p.PriorityClass = System.Diagnostics.ProcessPriorityClass.High;


此外,我們也可以利用 Windows 工作管理員來手動調整特定處理序的優先順序,如下圖所示:


執行緒的優先順序

決定為應用程式指定哪一種優先順序類別之後,接著要考慮的是應用程式中的執行緒。Windows 提供七種執行緒優先順序:閒置(Idle)、最低(Lowest)、低於正常(Below Normal)、正常(Normal)、高於正常(Above Normal)、最高(Highest)、時間緊迫(Time-Critical)。

六種處理序優先順序類別搭配七種執行緒優先順序,便能決定執行緒最終的優先等級。參考下表:


舉例來說,若某處理序的優先順序類別為 Normal,而該處理序中的某個執行緒的優先順序為 Above Normal,則該執行緒的實際優先等級為 9。處理序優先順序類別若為 Realtime,則其中的執行緒優先等級最起碼為 16。

以下程式片段示範如何設定執行緒的優先順序:

var t = new Thread(() => { Console.WriteLine("in worker thread"); });
t.Priority = ThreadPriority.Highest;

值得一提的是,.NET Framework  的 ThreadPriority 列舉型別僅定義了五種優先順序,缺了兩個:Idle 和 Time-Critical。為什麼 .NET 不提供這兩種執行緒優先順序呢?Jeffrey Richter 在他的《CLR via C# 第四版》中解釋:「如同 Windows 保留優先等級 0 和即時(real-time)等級範圍給自己,CLR 也保留了 Idle 和 Time-Critical 優先順序給自己使用。」

Windows 市集應用程式

以上討論並不適用於 Windows 市集應用程式,因為它既無法變更處理序的優先順序類別,也無法變更執行緒的優先順序。此外,當某個 Windows 市集應用程式從前景退居幕後成為背景程式時,Windows 會自動懸置該應用程式的所有執行緒。這麼做有兩個目的,一是避免背景應用程式拖慢前景應用程式的反應速度,讓使用者操作時更加流暢;二是減少 CPU 的負荷,從而節省電力耗損,提高電池的續航力。

小結

最後整理幾個重點觀念:
  • 使用多執行緒的目的主要有二:一是提升應用程式的回應速度(尤其是對 UI 操作的回應),二是提升應用程式的整體執行效能。這也意味著多執行緒應用程式往往有較佳的使用者體驗,而且更能善用 CPU 的強大運算能力。舉例來說, Visual Studio 會在你停止打字的時候在背後偷偷編譯你的程式碼,以便隨時提示語法錯誤;Word 會在你一邊打字的時候檢查拼字與文法,諸如此類的。
  • 「處理序優先順序類別」和「執行緒優先順序」只是用來簡化執行緒優先等級的設定,實際上 Windows 只會依執行緒的優先等級作出相應處置,而不會有「調整處理序優先順序」的動作。換言之,作用的對象是執行緒,不是處理序。
  • 對於一些背景處理的工作,例如背景即時編譯、拼字檢查等等,一般建議使用較低優先等級的執行緒來處理。如果是需要快速回應的工作,則可以考慮使用較高優先等級的執行緒。
參考資料

(謎之音:好想直接跳到 TPL 和 async/await 啊 Orz)

2 則留言:

  1. 老師,這段好像少了些內容:「應用程式應該只在真有必要時才用「高」優先類別,例如非。」

    回覆刪除
  2. 哎呀! 粗心大意竟給漏掉了....分時多工又共享變數的後果啊 XD。現已補上,感謝 Bruce 大! ^_^

    回覆刪除

技術提供:Blogger.
回頂端⬆️