《windows核心編程系列 》六談談線程調度、優先級和關聯性


線程調度、優先級和關聯性 

    每個線程都有一個CONTEXT結構,保存在線程內核對象中。大約每隔20ms windows就會查看所有當前存在的線程內核對象。並在可調度的線程內核對象中選擇一個,將其保存在CONTEXT結構的值載入cpu寄存器。這被稱為上下文切換。大約又過20ms  windows將當前cpu寄存器存回內核對象,線程被掛起。Windows再次檢查內核對象,並在可調度的內核對象中選擇一個進行調度。此過程不斷重復直到系統關閉。

 

    Windows被稱為搶占式多線程系統,系統可以在任何時刻停止一個線程而另行調度另外一個線程。我們對此可以有一些控制,但是權限很小。我們無法保證線程總在運行或者獲得整個處理器。

 

     由於windows並不是實時操作系統,我們無法保證在某一時間段一定在運行。

 

     一般情況下,系統中的可調度線程很少。因為它們都在等待某個事件的到來。比如notepad程序在等待用戶輸入的時候它就是不可調度的。它在等待鍵盤的輸入消息。當我們向notepad中輸入時也並不意味着notepad會立即獲得cpu時間。原因就是:windows並不是實時調度系統。

 

    線程內核對象中有一個值表示掛起計數。調用CreateThread時系統創建線程內核對象,並把掛起計數初始化為1。這樣cpu就不會調度它。在初始化之后,系統檢查是否有CREATE_SUSPEND標識傳入。如果有函數返回,進程仍保持掛起狀態。否則將掛起計數遞減為0,此時線程就可以被調度了。

 

    通過創建一個掛起的進程或線程我們可以在它們執行任何代碼前改變它們的環境,比如將其添加到作業中或是改變優先級。然后將它們設為可調度的。這可以通過調用ResumeThread函數。ResumeThread執行成功將返回前一次的掛起計數。失敗則返回0xFFFFFFFF。

 

    一個線程可以被掛起多次。除了在創建時傳入CREATE_SUSPEND標識外,還可以調用SuspendThread函數。第一個參數為想要掛起的線程句柄。任何線程都可以掛起另一個線程。掛起n次的線程要想變為可調度的必須調用ResumeThread()n次。

 

實際開發中,在調用SuspendThread時必須非常小心,因為我們無法知道線程此時在干什么。如果一個線程在分配堆中的內存,線程將鎖定堆,其他想要分配堆的線程將被掛起。直到第一個線程分配完畢。如果第一個線程被掛起,將會出現死鎖的情況。

 

    由於進程不是cpu調度的單位,所以不存在掛起進程的概念。但是我們可以掛起進程中所有的線程。可以調用調試器函數WaitForDebugEvent函數。恢復時可以調用ContinueDebugEvent。

 

    除了被別人調用SuspendThread掛起外,線程也可以告訴系統在一段時間內,可以將自己掛起,不需要調度。這可以調用Sleep實現。

    

void Sleep(DWORD dwMilliseconds); 


 

    參數表示線程自己掛起的時間。但是實際的掛起時間只是近似於所設定的參數。Windows並不是實時操作系統,不能保證線程可以准時醒來。實際時間取決於系統中其他線程的運行情況。當為其傳入0時,表示主調線程主動放棄本次時間片的剩余部分。注意是本次。

 

    系統提供一個名為SwitchToThread函數,如果存在另一個可調度的線程,那么系統將讓此線程運行。

    BOOL SwitchToThread();

    調用此函數時,系統查看是否存在急需cpu時間的飢餓線程。如沒有則函數返回。如果存在,SwitchToThread將調度該線程。它與Sleep(0)很相似。區別在於SwitchToThread允許執行低優先級線程。

 

    當我們需要計算線程執行某項任務的我的時間時,很多人習慣使用GetTickCount64函數。

 

  1.  
    ULONG start=GetTickCount64();
  2.  
     
  3.  
    //do something.
  4.  
     
  5.  
    ULONG end=GetTickCount64();
  6.  
     
  7.  
     


 

 此段代碼有個前提就是代碼執行不會被中斷。但是在搶占式OS中,線程可以隨時被終止。執行上述代碼的線程可能在執行第一個函數后就被掛起。一段時間后再次被調度。這時候時間就不准確了。

 

    Windows提供了一個函數可以返回一個線程以獲得cpu時間:   

   

  1.  
    BOOL GetThreadTime(
  2.  
     
  3.  
    HANDLE hThread,
  4.  
     
  5.  
    PFILETIME pftCreationTime,
  6.  
     
  7.  
    PFILETIME pftExitTime,
  8.  
     
  9.  
    PFILETIME pftKernelTime,
  10.  
     
  11.  
    PFILETIME pftUserTime);
  12.  
     
  13.  
     


 

   第一個參數為想獲得的線程句柄。

    第二個參數返回(線程創建時間-1601年1月1日0:00)的秒數。單位是100ns。

    第三個表示退出時間-1601年1月1日0:00的秒數。單位是100ns。

    第四個表示線程執行內核模式下的時間的絕對值。單位是100ns。

    第五個表示線程執行用戶模式代碼的時間的絕對值。單位是100ns。

    類似的,GetProcessTime可以返回進程中所有線程的時間之和。

    在進行高精度的計算時上述函數仍然不夠。此時windows提供了以下函數:

   

  1.  
    BOOL QueryPerformanceFrequency(LARGE_INTEGER *pliFrequency)
  2.  
     
  3.  
    BOOL QueryPerformanceCounter(LARGE_INTEGER *pliCount);
  4.  
     
  5.  
     


 

    這兩個函數假設正在執行的線程不會被搶占。它們都是針對生命期很短的代碼塊。GetCPUFrequencyInMHZ可以獲得cpu頻率。

    在windows定義的所有數據結構中,CONTEXT結構是唯一一個依賴於cpu的。我們可以通過調用GetThreadContext來獲得當期cpu寄存器的狀態。

   

  1.  
    BOOL GetThreadContext(
  2.  
     
  3.  
    HANDLE pThread,
  4.  
     
  5.  
    PCONTEXT pContext);
  6.  
     


 

   第二個參數是CONTEXT結構指針。在分配CONTEXT結構后,需要初始化ContextFlag標志,表示以表示要獲取哪些寄存器。函數執行后CONTEXT對象中就填入我們請求的成員。ContextFlag可以是:

CONTEXT_CONTROL表示控制寄存器。

    CONTEXT_INTEGER表示整數寄存器。

    CONTEXT_FLOAT 表示浮點寄存器。

    CONTEXT_ALL 表示CONTEXT_CONTROL |CONTEXT_INTEGER|CONTEXT_SEGMENTS。

    在調用GetThreadContext時,需要先調用SuspendThread。因為在調用GetThreadContext時系統可能正在執行那個線程,此時線程的上下文與獲得的信息就不一致了。注意,它只能返回線程的用戶模式上下文。如果當調用SuspendThread時線程正在內核模式運行,線程不會暫停,直到其返回用戶空間。但是返回到用戶控件后不會執行任何用戶模式代碼。

    不僅僅能獲得線程的進程上下文,我們還可以設置它。這可以調用:

   

  1.  
    BOOL SetThreadContext()
  2.  
     
  3.  
    HANDLE hThread,
  4.  
     
  5.  
    CONST CONTEXT *pContext);
  6.  
     


 

    GetThreadContext和SetThreadContext函數為我們提供了對線程許多控制的方法,但是需要小心使用。

    線程優先級

    前面提到的調度程序在調度另外一個線程之前,可以運行一個線程大約20ms的時間。但是這是所有優先級都相同的情況。實際上系統中的很多線程優先級是不同的,這將影響調度程序如何選擇下一個要運行的線程。

    Windows的線程優先級從0到31。每個線程都會分配一個優先級。當系統確定給哪個線程分配cpu時,它會首先查看優先級為31的線程,直至所有優先級為31的線程都被調度。然后再查看下一優先級線程。只要存在優先級為31的線程,系統就不會調度0-30級的線程。低優先級線程長時間得不到cpu時間,這被稱為飢餓。這不經常出現,因為大多數線程都是不可調度的。

    系統啟動時會創建一個優先級為0的idle線程,整個系統只有它的優先級為0。它在系統中沒有其他線程運行時將系統內存中所有閑置頁面清0。

    Windows中的線程優先級是由優先級類和相對線程優先級來確定的。系統通過線程的相對優先級加上線程所屬進程的優先級來確定線程的優先級值。這個值被稱為線程的基本優先級值。

    Windows支持6個進程優先級類:idle ,below normal ,normal ,above normal,high和real-time。它們是相對與進程的。Normal最為常用,為99%的進程使用。

    idle優先級類在系統什么都不做的時候運行的應用程序。如屏幕保護程序。real-time優先級類優先級別最高,但是沒有開放給用戶使用。因為此優先級類的程序會影響操作系統的任務。

    Windows支持7個相對線程優先級:idle,lowest ,below normal,normal,above normal,highest和time-critical。這些優先級是相對於進程優先級的。大多數的線程使用normal優先級。

    概括起來就是進程屬於某個優先級類,另外還可以指定進程中線程的相對線程優先級。也就是說線程優先級是相對於進程優先級的。time-critical優先級對於real-time優先級類,優先級為31。相對於其他優先級類則為15。

    需要注意的是進程優先級是抽象的概念,因為進程並不參與調度。

在優先級編程時,首先需要在調用CreateProcess時可以再fdwCreate參數中傳入想要的優先級。fdwCreate可以是以下標識符:

real-time        REALTIME_PRIORITY_CLASS

high            HIGH_PRIORITY_CLASS

above normal     ABOVE_NORMAL_PRIORITY_CLASS

normal          NORMAL_PRIORITY_CLASS

below_normal    BELOW_NORMAL_PRIORITY_CLASS

idle             IDLE_PRIORITY_CLASS

 

進程運行后可以調用SetPrioritClass來改變進程優先級類。

    

  1.  
    BOOL SetPriorityClass(
  2.  
     
  3.  
    HANDLE hProcess,
  4.  
     
  5.  
    DWORD fdwPriority);
  6.  
     
  7.  
     


 

   可以調用GetPriorityClass來獲得進程的優先級類。

    DWORD GetPriorityClass(HANDLE hProcess);

    上面是指定的進程優先級類,調用CreateThread創建線程時,它的線程優先級總是被設置為normal。可以調用以下函數來改變線程優先級:

    

  1.  
    BOOL SetThreadPriority(
  2.  
     
  3.  
    HANDLE hThread,
  4.  
     
  5.  
    int nPriority);
  6.  
     


 

  nPriority可以是以下標識符:

time-critical       THREAD_PRIORITY_TIME_CRITICAL

highest           THREAD_PRIORITY_HIGHEST

above-normal      THREAD_PRIORITY_ABOVE_NORMAL

normal           THREAD_PRIORITY_NORMAL

below-normal      THREAD_PRIORITY_BELOW_NORMAL

lowest            THREAD_PRIORITY_LOWEST

idle              THREAD_PRIORITY_IDLE

   但是在調用CreateThread時需要傳入CREATE_SUSPEND,使線程暫停執行。

    相應的可以調用int GetThreadPriority(HANDLE hThread);返回線程相對優先級。

     Windows並沒有返回線程優先級的函數,而是分別提供返回進程優先級類和相對線程優先級。

    有些時候,系統也會提升一個線程的優先級。比如某個線程正在等待用戶按鍵消息。當用戶敲了一個鍵,系統會在線程的消息隊列中放入一個WM_KEYDOWN消息。此時線程就變成可調度的了。鍵盤設備驅動程序將使系統臨時提升線程的優先級。在該時間片結束后,系統會將線程的優先級值減一,第三個時間片執行時再減去一。直至保持基本優先級運行。

    注意:線程的當前優先級不會低於進程的基本優先級。而且設備驅動程序可以決定動態提升的幅度。系統只提升優先級值在1~15的線程。這個范圍被稱為動態優先級范圍。可以通過調用以下函數來禁止系統對線程優先級進行動態 提升:

    

  1.  
    BOOL SetProcessPriorityBoost(
  2.  
     
  3.  
    HANDLE hProcess,
  4.  
     
  5.  
    BOOL bDisablePriorityBoost);
  6.  
     


 

    此函數禁止動態提升此進程內的所有線程的優先級。

    

  1.  
    BOOL SetThreadPriorityBoost(
  2.  
     
  3.  
    HANDLE hThread,
  4.  
     
  5.  
    BOOL bDisablePriorityBoost);
  6.  
     


 

    此函數禁止動態提升某個線程的優先級。

還有一種動態提升優先級的情況:檢測到有飢餓情況出現時,也就是某個線程由於優先級低,而長時間無法得到調度時。系統就會動態提升此線程的優先級。系統允許它運行兩個時間片。兩個時間片結束之后立即恢復到基本優先級。

用戶正在使用的窗口被稱為前台窗口。這個進程就被稱為前台進程。為了改進前台進程的響應性,windows會為前台進程中的線程微調調度算法。是前台進程的線程分配比一般情況下更多的時間片。

    關聯性

    默認情況下,windows在分配cpu時采用軟關聯的方式。也就是說在其他因素相同的情況下,系統使線程在上一次運行的處理器上運行。這有助於重用仍在處理器高速緩存中的數據。

系統在啟動時確定cpu數量。應用程序可以通過調用GetSysInfo來查詢cpu的數量。如果需要限制一個進程的所有線程在某些cpu上運行,可以調用:

  1.  
    BOOL SetProcessAffinityMask(
  2.  
     
  3.  
    HANDLE hProcess,
  4.  
     
  5.  
    DWORD_PTR dwProcessAffinityMask);
  6.  
     


 

第一個參數代表要設置的進程句柄。

第二參數是一個位掩碼。代表線程可以在哪些cpu上運行。

注意子進程將繼承父進程的關聯性。

    GetProcessAffinityMask返回進程的關聯掩碼。

    相應的還可以設置某個線程只在一組cpu上運行:

    SetThreadAffinityMask。

    有時候強制一個線程只在某個特定的cpu上運行並不是什么好主意。Windows允許一個線程運行在一個cpu上,但如果需要,它將被移動到一個空閑的cpu上。

    要給線程設置一個理想的cpu,可以調用:

    

  1.  
    DWORD SetThreadIdealProcessro(
  2.  
     
  3.  
    HANDLE hThread
  4.  
     
  5.  
    DWORD dwIdealProcessor);
  6.  
     


 

    dwIdealProcessor是一個0到31/63之間的整數。表示線程希望設置的cpu。可以傳入MAXIMUM_PROCESSOR值,表示沒有理想的cpu。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM