線程調(diào)度、優(yōu)先級和關(guān)聯(lián)性
每個線程都有一個CONTEXT結(jié)構(gòu),保存在線程內(nèi)核對象中。大約每隔20ms windows就會查看所有當前存在的線程內(nèi)核對象。并在可調(diào)度的線程內(nèi)核對象中選擇一個,將其保存在CONTEXT結(jié)構(gòu)的值載入cpu寄存器。這被稱為上下文切換。大約又過20ms windows將當前cpu寄存器存回內(nèi)核對象,線程被掛起。Windows再次檢查內(nèi)核對象,并在可調(diào)度的內(nèi)核對象中選擇一個進行調(diào)度。此過程不斷重復(fù)直到系統(tǒng)關(guān)閉。
Windows被稱為搶占式多線程系統(tǒng),系統(tǒng)可以在任何時刻停止一個線程而另行調(diào)度另外一個線程。我們對此可以有一些控制,但是權(quán)限很小。我們無法保證線程總在運行或者獲得整個處理器。
由于windows并不是實時操作系統(tǒng),我們無法保證在某一時間段一定在運行。
一般情況下,系統(tǒng)中的可調(diào)度線程很少。因為它們都在等待某個事件的到來。比如notepad程序在等待用戶輸入的時候它就是不可調(diào)度的。它在等待鍵盤的輸入消息。當我們向notepad中輸入時也并不意味著notepad會立即獲得cpu時間。原因就是:windows并不是實時調(diào)度系統(tǒng)。
線程內(nèi)核對象中有一個值表示掛起計數(shù)。調(diào)用CreateThread時系統(tǒng)創(chuàng)建線程內(nèi)核對象,并把掛起計數(shù)初始化為1。這樣cpu就不會調(diào)度它。在初始化之后,系統(tǒng)檢查是否有CREATE_SUSPEND標識傳入。如果有函數(shù)返回,進程仍保持掛起狀態(tài)。否則將掛起計數(shù)遞減為0,此時線程就可以被調(diào)度了。
通過創(chuàng)建一個掛起的進程或線程我們可以在它們執(zhí)行任何代碼前改變它們的環(huán)境,比如將其添加到作業(yè)中或是改變優(yōu)先級。然后將它們設(shè)為可調(diào)度的。這可以通過調(diào)用ResumeThread函數(shù)。ResumeThread執(zhí)行成功將返回前一次的掛起計數(shù)。失敗則返回0xFFFFFFFF。
一個線程可以被掛起多次。除了在創(chuàng)建時傳入CREATE_SUSPEND標識外,還可以調(diào)用SuspendThread函數(shù)。第一個參數(shù)為想要掛起的線程句柄。任何線程都可以掛起另一個線程。掛起n次的線程要想變?yōu)榭烧{(diào)度的必須調(diào)用ResumeThread()n次。
實際開發(fā)中,在調(diào)用SuspendThread時必須非常小心,因為我們無法知道線程此時在干什么。如果一個線程在分配堆中的內(nèi)存,線程將鎖定堆,其他想要分配堆的線程將被掛起。直到第一個線程分配完畢。如果第一個線程被掛起,將會出現(xiàn)死鎖的情況。
由于進程不是cpu調(diào)度的單位,所以不存在掛起進程的概念。但是我們可以掛起進程中所有的線程。可以調(diào)用調(diào)試器函數(shù)WaitForDebugEvent函數(shù)。恢復(fù)時可以調(diào)用ContinueDebugEvent。
除了被別人調(diào)用SuspendThread掛起外,線程也可以告訴系統(tǒng)在一段時間內(nèi),可以將自己掛起,不需要調(diào)度。這可以調(diào)用Sleep實現(xiàn)。
- void Sleep(DWORD dwMilliseconds);
參數(shù)表示線程自己掛起的時間。但是實際的掛起時間只是近似于所設(shè)定的參數(shù)。Windows并不是實時操作系統(tǒng),不能保證線程可以準時醒來。實際時間取決于系統(tǒng)中其他線程的運行情況。當為其傳入0時,表示主調(diào)線程主動放棄本次時間片的剩余部分。注意是本次。
系統(tǒng)提供一個名為SwitchToThread函數(shù),如果存在另一個可調(diào)度的線程,那么系統(tǒng)將讓此線程運行。
BOOL SwitchToThread();
調(diào)用此函數(shù)時,系統(tǒng)查看是否存在急需cpu時間的饑餓線程。如沒有則函數(shù)返回。如果存在,SwitchToThread將調(diào)度該線程。它與Sleep(0)很相似。區(qū)別在于SwitchToThread允許執(zhí)行低優(yōu)先級線程。
當我們需要計算線程執(zhí)行某項任務(wù)的我的時間時,很多人習慣使用GetTickCount64函數(shù)。
- ULONG start=GetTickCount64();
-
- //do something.
-
- ULONG end=GetTickCount64();
-
-
此段代碼有個前提就是代碼執(zhí)行不會被中斷。但是在搶占式OS中,線程可以隨時被終止。執(zhí)行上述代碼的線程可能在執(zhí)行第一個函數(shù)后就被掛起。一段時間后再次被調(diào)度。這時候時間就不準確了。
Windows提供了一個函數(shù)可以返回一個線程以獲得cpu時間:
- BOOL GetThreadTime(
-
- HANDLE hThread,
-
- PFILETIME pftCreationTime,
-
- PFILETIME pftExitTime,
-
- PFILETIME pftKernelTime,
-
- PFILETIME pftUserTime);
-
-
第一個參數(shù)為想獲得的線程句柄。
第二個參數(shù)返回(線程創(chuàng)建時間-1601年1月1日0:00)的秒數(shù)。單位是100ns。
第三個表示退出時間-1601年1月1日0:00的秒數(shù)。單位是100ns。
第四個表示線程執(zhí)行內(nèi)核模式下的時間的絕對值。單位是100ns。
第五個表示線程執(zhí)行用戶模式代碼的時間的絕對值。單位是100ns。
類似的,GetProcessTime可以返回進程中所有線程的時間之和。
在進行高精度的計算時上述函數(shù)仍然不夠。此時windows提供了以下函數(shù):
- BOOL QueryPerformanceFrequency(LARGE_INTEGER *pliFrequency)
-
- BOOL QueryPerformanceCounter(LARGE_INTEGER *pliCount);
-
-
這兩個函數(shù)假設(shè)正在執(zhí)行的線程不會被搶占。它們都是針對生命期很短的代碼塊。GetCPUFrequencyInMHZ可以獲得cpu頻率。
在windows定義的所有數(shù)據(jù)結(jié)構(gòu)中,CONTEXT結(jié)構(gòu)是唯一一個依賴于cpu的。我們可以通過調(diào)用GetThreadContext來獲得當期cpu寄存器的狀態(tài)。
- BOOL GetThreadContext(
-
- HANDLE pThread,
-
- PCONTEXT pContext);
第二個參數(shù)是CONTEXT結(jié)構(gòu)指針。在分配CONTEXT結(jié)構(gòu)后,需要初始化ContextFlag標志,表示以表示要獲取哪些寄存器。函數(shù)執(zhí)行后CONTEXT對象中就填入我們請求的成員。ContextFlag可以是:
CONTEXT_CONTROL表示控制寄存器。
CONTEXT_INTEGER表示整數(shù)寄存器。
CONTEXT_FLOAT 表示浮點寄存器。
CONTEXT_ALL 表示CONTEXT_CONTROL |CONTEXT_INTEGER|CONTEXT_SEGMENTS。
在調(diào)用GetThreadContext時,需要先調(diào)用SuspendThread。因為在調(diào)用GetThreadContext時系統(tǒng)可能正在執(zhí)行那個線程,此時線程的上下文與獲得的信息就不一致了。注意,它只能返回線程的用戶模式上下文。如果當調(diào)用SuspendThread時線程正在內(nèi)核模式運行,線程不會暫停,直到其返回用戶空間。但是返回到用戶控件后不會執(zhí)行任何用戶模式代碼。
不僅僅能獲得線程的進程上下文,我們還可以設(shè)置它。這可以調(diào)用:
- BOOL SetThreadContext()
-
- HANDLE hThread,
-
- CONST CONTEXT *pContext);
GetThreadContext和SetThreadContext函數(shù)為我們提供了對線程許多控制的方法,但是需要小心使用。
線程優(yōu)先級
前面提到的調(diào)度程序在調(diào)度另外一個線程之前,可以運行一個線程大約20ms的時間。但是這是所有優(yōu)先級都相同的情況。實際上系統(tǒng)中的很多線程優(yōu)先級是不同的,這將影響調(diào)度程序如何選擇下一個要運行的線程。
Windows的線程優(yōu)先級從0到31。每個線程都會分配一個優(yōu)先級。當系統(tǒng)確定給哪個線程分配cpu時,它會首先查看優(yōu)先級為31的線程,直至所有優(yōu)先級為31的線程都被調(diào)度。然后再查看下一優(yōu)先級線程。只要存在優(yōu)先級為31的線程,系統(tǒng)就不會調(diào)度0-30級的線程。低優(yōu)先級線程長時間得不到cpu時間,這被稱為饑餓。這不經(jīng)常出現(xiàn),因為大多數(shù)線程都是不可調(diào)度的。
系統(tǒng)啟動時會創(chuàng)建一個優(yōu)先級為0的idle線程,整個系統(tǒng)只有它的優(yōu)先級為0。它在系統(tǒng)中沒有其他線程運行時將系統(tǒng)內(nèi)存中所有閑置頁面清0。
Windows中的線程優(yōu)先級是由優(yōu)先級類和相對線程優(yōu)先級來確定的。系統(tǒng)通過線程的相對優(yōu)先級加上線程所屬進程的優(yōu)先級來確定線程的優(yōu)先級值。這個值被稱為線程的基本優(yōu)先級值。
Windows支持6個進程優(yōu)先級類:idle ,below normal ,normal ,above normal,high和real-time。它們是相對與進程的。Normal最為常用,為99%的進程使用。
idle優(yōu)先級類在系統(tǒng)什么都不做的時候運行的應(yīng)用程序。如屏幕保護程序。real-time優(yōu)先級類優(yōu)先級別最高,但是沒有開放給用戶使用。因為此優(yōu)先級類的程序會影響操作系統(tǒng)的任務(wù)。
Windows支持7個相對線程優(yōu)先級:idle,lowest ,below normal,normal,above normal,highest和time-critical。這些優(yōu)先級是相對于進程優(yōu)先級的。大多數(shù)的線程使用normal優(yōu)先級。
概括起來就是進程屬于某個優(yōu)先級類,另外還可以指定進程中線程的相對線程優(yōu)先級。也就是說線程優(yōu)先級是相對于進程優(yōu)先級的。time-critical優(yōu)先級對于real-time優(yōu)先級類,優(yōu)先級為31。相對于其他優(yōu)先級類則為15。
需要注意的是進程優(yōu)先級是抽象的概念,因為進程并不參與調(diào)度。
在優(yōu)先級編程時,首先需要在調(diào)用CreateProcess時可以再fdwCreate參數(shù)中傳入想要的優(yōu)先級。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
進程運行后可以調(diào)用SetPrioritClass來改變進程優(yōu)先級類。
- BOOL SetPriorityClass(
-
- HANDLE hProcess,
-
- DWORD fdwPriority);
-
-
可以調(diào)用GetPriorityClass來獲得進程的優(yōu)先級類。
DWORD GetPriorityClass(HANDLE hProcess);
上面是指定的進程優(yōu)先級類,調(diào)用CreateThread創(chuàng)建線程時,它的線程優(yōu)先級總是被設(shè)置為normal??梢哉{(diào)用以下函數(shù)來改變線程優(yōu)先級:
- BOOL SetThreadPriority(
-
- HANDLE hThread,
-
- int nPriority);
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
但是在調(diào)用CreateThread時需要傳入CREATE_SUSPEND,使線程暫停執(zhí)行。
相應(yīng)的可以調(diào)用int GetThreadPriority(HANDLE hThread);返回線程相對優(yōu)先級。
Windows并沒有返回線程優(yōu)先級的函數(shù),而是分別提供返回進程優(yōu)先級類和相對線程優(yōu)先級。
有些時候,系統(tǒng)也會提升一個線程的優(yōu)先級。比如某個線程正在等待用戶按鍵消息。當用戶敲了一個鍵,系統(tǒng)會在線程的消息隊列中放入一個WM_KEYDOWN消息。此時線程就變成可調(diào)度的了。鍵盤設(shè)備驅(qū)動程序?qū)⑹瓜到y(tǒng)臨時提升線程的優(yōu)先級。在該時間片結(jié)束后,系統(tǒng)會將線程的優(yōu)先級值減一,第三個時間片執(zhí)行時再減去一。直至保持基本優(yōu)先級運行。
注意:線程的當前優(yōu)先級不會低于進程的基本優(yōu)先級。而且設(shè)備驅(qū)動程序可以決定動態(tài)提升的幅度。系統(tǒng)只提升優(yōu)先級值在1~15的線程。這個范圍被稱為動態(tài)優(yōu)先級范圍。可以通過調(diào)用以下函數(shù)來禁止系統(tǒng)對線程優(yōu)先級進行動態(tài) 提升:
- BOOL SetProcessPriorityBoost(
-
- HANDLE hProcess,
-
- BOOL bDisablePriorityBoost);
此函數(shù)禁止動態(tài)提升此進程內(nèi)的所有線程的優(yōu)先級。
- BOOL SetThreadPriorityBoost(
-
- HANDLE hThread,
-
- BOOL bDisablePriorityBoost);
此函數(shù)禁止動態(tài)提升某個線程的優(yōu)先級。
還有一種動態(tài)提升優(yōu)先級的情況:檢測到有饑餓情況出現(xiàn)時,也就是某個線程由于優(yōu)先級低,而長時間無法得到調(diào)度時。系統(tǒng)就會動態(tài)提升此線程的優(yōu)先級。系統(tǒng)允許它運行兩個時間片。兩個時間片結(jié)束之后立即恢復(fù)到基本優(yōu)先級。
用戶正在使用的窗口被稱為前臺窗口。這個進程就被稱為前臺進程。為了改進前臺進程的響應(yīng)性,windows會為前臺進程中的線程微調(diào)調(diào)度算法。是前臺進程的線程分配比一般情況下更多的時間片。
關(guān)聯(lián)性
默認情況下,windows在分配cpu時采用軟關(guān)聯(lián)的方式。也就是說在其他因素相同的情況下,系統(tǒng)使線程在上一次運行的處理器上運行。這有助于重用仍在處理器高速緩存中的數(shù)據(jù)。
系統(tǒng)在啟動時確定cpu數(shù)量。應(yīng)用程序可以通過調(diào)用GetSysInfo來查詢cpu的數(shù)量。如果需要限制一個進程的所有線程在某些cpu上運行,可以調(diào)用:
- BOOL SetProcessAffinityMask(
-
- HANDLE hProcess,
-
- DWORD_PTR dwProcessAffinityMask);
第一個參數(shù)代表要設(shè)置的進程句柄。
第二參數(shù)是一個位掩碼。代表線程可以在哪些cpu上運行。
注意子進程將繼承父進程的關(guān)聯(lián)性。
GetProcessAffinityMask返回進程的關(guān)聯(lián)掩碼。
相應(yīng)的還可以設(shè)置某個線程只在一組cpu上運行:
SetThreadAffinityMask。
有時候強制一個線程只在某個特定的cpu上運行并不是什么好主意。Windows允許一個線程運行在一個cpu上,但如果需要,它將被移動到一個空閑的cpu上。
要給線程設(shè)置一個理想的cpu,可以調(diào)用:
- DWORD SetThreadIdealProcessro(
-
- HANDLE hThread
-
- DWORD dwIdealProcessor);
dwIdealProcessor是一個0到31/63之間的整數(shù)。表示線程希望設(shè)置的cpu??梢詡魅?span style="font-family:Times New Roman">MAXIMUM_PROCESSOR值,表示沒有理想的cpu。
2012、9、28于山西大同
|