7线程的调度优先级和亲缘性.docx
《7线程的调度优先级和亲缘性.docx》由会员分享,可在线阅读,更多相关《7线程的调度优先级和亲缘性.docx(43页珍藏版)》请在冰豆网上搜索。
![7线程的调度优先级和亲缘性.docx](https://file1.bdocx.com/fileroot1/2023-1/22/2a9280f8-6f81-4284-954e-9504e0b4a71e/2a9280f8-6f81-4284-954e-9504e0b4a71e1.gif)
7线程的调度优先级和亲缘性
第7章线程的调度、优先级和亲缘性
抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间。
本章将要介绍MicrosoftWindows98和Windows2000使用的一些算法。
上一章介绍了每个线程是如何拥有一个上下文结构的,这个结构维护在线程的内核对象中。
这个上下文结构反映了线程上次运行时该线程的CPU寄存器的状态。
每隔20ms左右,Windows要查看当前存在的所有线程内核对象。
在这些对象中,只有某些对象被视为可以调度的对象。
Windows选择可调度的线程内核对象中的一个,将它加载到CPU的寄存器中,它的值是上次保存在线程的环境中的值。
这项操作称为上下文转换。
Windows实际上保存了一个记录,它说明每个线程获得了多少个运行机会。
使用MicrosoftSpy++这个工具,就可以了解这个情况。
图7-1显示了一个线程的属性。
注意,该线程已经被调度了37379次。
目前,线程正在执行代码,并对它的进程的地址空间中的数据进行操作。
再过20ms左右,Windows就将CPU的寄存器重新保存到线程的上下文中。
线程不再运行。
系统再次查看其余的可调度线程内核对象,选定另一个线程的内核对象,将该线程的上下文加载到CPU的寄存器中,然后继续运行。
当系统引导时,便开始加载线程的上下文,让线程运行,保存上下文和重复这些操作,直到系统关闭。
图7-1线程的属性
总之,这就是系统对线程进行调度的过程。
这很简单,是不是?
Windows被称为抢占式多线程操作系统,因为一个线程可以随时停止运行,随后另一个线程可进行调度。
如你所见,可以对它进行一定程度的控制,但是不能太多。
记住,无法保证线程总是能够运行,也不能保证线程能够得到整个进程,无法保证其他线程不被允许运行等等。
注意程序员常常问我,如何才能保证线程在某个事件的某个时间段内开始运行,比如,如何才能确保某个线程在数据从串行端口传送过来的1ms内开始运行呢?
我的回答是,办不到。
实时操作系统才能作出这样的承诺,但是Windows不是实时操作系统。
实时操作系统必须清楚地知道它是在什么硬件上运行,这样它才能知道它的硬盘控制器和键盘等的等待时间。
Microsoft对Windows规定的目标是,使它能够在各种不同的硬件上运行,即能够在不同的CPU、不同的驱动器和不同的网络上运行。
简而言之,Windows没有设计成为一种实时操作系统。
尽管应强调这样一个概念,即系统只调度可以调度的线程,但是实际情况是,系统中的大多数线程是不可调度的线程。
例如,有些线程对象的暂停计数大于1。
这意味着该线程已经暂停运行,不应该给它安排任何CPU时间。
通过调用使用CREATE_SUSPENDED标志的CreateProcess或CreateThread函数,可以创建一个暂停的线程。
(本章后面还要介绍SuspendThread和ResumeThread函数。
)
除了暂停的线程外,其他许多线程也是不可调度的线程,因为它们正在等待某些事情的发生。
例如,如果运行Notepad,但是并不键入任何数据,那么Notepad的线程就没有什么事情要做。
系统不给无事可做的线程分配CPU时间。
当移动Notepad的窗口时,或者Notepad的窗口需要刷新它的内容,或者将数据键入Notepad,系统就会自动使Notepad的线程成为可调度的线程。
这并不意味着Notepad的线程立即获得了CPU时间。
它只是表示Notepad的线程有事情可做,系统将设法在某个时间(不久的将来)对它进行调度。
7.1暂停和恢复线程的运行
在线程内核对象的内部有一个值,用于指明线程的暂停计数。
当调用CreateProcess或CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。
这可以防止线程被调度到CPU中。
当然,这是很有用的,因为线程的初始化需要时间,你不希望在系统做好充分的准备之前就开始执行线程。
当线程完全初始化好了之后,CreateProcess或CreateThread要查看是否已经传递了CREATE_SUSPENDED标志。
如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。
如果尚未传递该标志,那么该函数将线程的暂停计数递减为0。
当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。
在暂停状态中创建一个线程,就能够在线程有机会执行任何代码之前改变线程的运行环境(如优先级)。
一旦改变了线程的环境,必须使线程成为可调度线程。
要进行这项操作,可以调用ResumeThread,将调用CreateThread函数时返回的线程句柄传递给它(或者是将传递给CreateProcess的ppiProcInfo参数指向的线程句柄传递给它):
DWORDResumeThread(HANDLEhThread);
如果ResumeThread函数运行成功,它将返回线程的前一个暂停计数,否则返回0xFFFFFFFF。
单个线程可以暂停若干次。
如果一个线程暂停了3次,它必须恢复3次,然后它才可以被分配给一个CPU。
当创建线程时,除了使用CREATE_SUSPENDED外,也可以调用SuspendThread函数来暂停线程的运行:
DWORDSuspendThread(HANDLEhThread);
任何线程都可以调用该函数来暂停另一个线程的运行(只要拥有线程的句柄)。
不用说,线程可以自行暂停运行,但是不能自行恢复运行。
与ResumeThread一样,SuspendThread返回的是线程的前一个暂停计数。
线程暂停的最多次数可以是MAXIMUM_SUSPEND_COUNT次(在WinNT.h中定义为127)。
注意,SuspendThread与内核方式的执行是异步进行的,但是在线程恢复运行之前,不会发生用户方式的执行。
在实际环境中,调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。
如果线程试图从堆栈中分配内存,那么该线程将在该堆栈上设置一个锁。
当其他线程试图访问该堆栈时,这些线程的访问就被停止,直到第一个线程恢复运行。
只有确切知道目标线程是什么(或者目标线程正在做什么),并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的(死锁和其他线程同步问题将在第8、9和10章介绍)。
7.2暂停和恢复进程的运行
对于Windows来说,不存在暂停或恢复进程的概念,因为进程从来不会被安排获得CPU时间。
但是,曾经有人无数次问我如何暂停进程中的所有线程的运行。
Windows确实允许一个进程暂停另一个进程中的所有线程的运行,但是从事暂停操作的进程必须是个调试程序。
特别是,进程必须调用WaitForDebugEvent和ContinueDebugEvent之类的函数。
由于竞争的原因,Windows没有提供其他方法来暂停进程中所有线程的运行。
例如,虽然许多线程已经暂停,但是仍然可以创建新线程。
从某种意义上说,系统必须在这个时段内暂停所有新线程的运行。
Microsoft已经将这项功能纳入了系统的调试机制。
虽然无法创建绝对完美的SuspendProcess函数,但是可以创建一个该函数的实现代码,它能够在许多条件下出色地运行。
下面是我的SuspendProcess函数的实现代码:
VOIDSuspendProcess(DWORDdwProcessID,BOOLfSuspend)
{
//Getthelistofthreadsinthesystem.
HANDLEhSnapshot=CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD,dwProcessID);
if(hSnapshot!
=INVALID_HANDLE_VALUE)
{
//Walkthelistofthreads.
THREADENTRY32te={sizeof(te)};
BOOLfOk=Thread32First(hSnapshot,&te);
for(;fOk;fOk=Thread32Next(hSnapshot,&te))
{
//Isthisthreadinthedesiredprocess?
if(te.th32OwnerProcessID==dwProcessID)
{
//AttempttoconvertthethreadIDintoahandle.
HANDLEhThread=OpenThread(THREAD_SUSPEND_RESUME,
FALSE,te.th32ThreadID);
if(hThread!
=NULL)
{
//Suspendorresumethethread.
if(fSuspend)
SuspendThread(hThread);
else
ResumeThread(hThread);
}
CloseHandle(hThread);
}
}
CloseHandle(hSnapshot);
}
}
我的SuspendProcess函数使用ToolHelp函数来枚举系统中的线程列表。
当我找到作为指定进程的组成部分的线程时,我调用OpenThread:
HANDLEOpenThread(DWORDdwDesiredAccess,
BOOLbInheritHandle,DWORDdwThreadID);
这个新Windows2000函数负责找出带有匹配的线程ID的线程内核对象,对内核对象的使用计数进行递增,然后返回对象的句柄。
运用这个句柄,我调用SuspendThread(或ResumeThread)。
由于OpenThread在Windows2000中是个新函数,因此我的SuspendProcess函数在Windows95或Windows98上无法运行,在WindowsNT4.0或更早的版本上也无法运行。
也许你懂得为什么SuspendProcess不能总是运行,原因是当枚举线程组时,新线程可以被创建和撤消。
因此,当我调用CreateToolhelp32Snapshot后,一个新线程可能会出现在目标进程中,我的函数将无法暂停这个新线程。
过了一些时候,当调用SuspendProcess函数来恢复线程的运行时,它将恢复它从未暂停的一个线程的运行。
更糟糕的是,当枚举线程ID时,一个现有的线程可能被撤消,一个新线程可能被创建,这两个线程可能拥有相同的ID。
这将会导致该函数暂停任意些个(也许在目标进程之外的一个进程中的)线程的运行。
当然,这些情况不太可能出现。
如果非常了解目标进程是如何运行的,那么这些问题也许根本不是问题。
我提供这个函数供酌情使用。
7.3睡眠方式
线程也能告诉系统,它不想在某个时间段内被调度。
这是通过调用Sleep函数来实现的:
VOIDSleep(DWORDdwMilliseconds);
该函数可使线程暂停自己的运行,直到dwMilliseconds过去为止。
关于Sleep函数,有下面几个重要问题值得注意:
•调用Sleep,可使线程自愿放弃它剩余的时间片。
•系统将在大约的指定毫秒数内使线程不可调度。
不错,如果告诉系统,想睡眠100ms,那么可以睡眠大约这么长时间,但是也可能睡眠数秒钟或者数分钟。
记住,Windows不是个实时操作系统。
虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统中还有什么操作正在进行。
•可以调用Sleep,并且为dwMilliseconds参数传递INFINITE。
这将告诉系统永远不要调度该线程。
这不是一件值得去做的事情。
最好是让线程退出,并还原它的堆栈和内核对象。
•可以将0传递给Sleep。
这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。
但是,系统可以对刚刚调用Sleep的线程重新调度。
如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。
7.4转换到另一个线程
系统提供了一个称为SwitchToThread的函数,使得另一个可调度线程(如果存在能够运行):
BOOLSwitchToThread();
当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。
如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。
如果存在一个迫切需要CPU时间的线程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。
这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。
该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。
如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。
调用SwitchToThread函数与调用Sleep是相似的,并且传递给它一个0ms的超时。
差别是SwitchToThread允许优先级较低的线程运行。
即使低优先级线程迫切需要CPU时间,Sleep也能够立即对调用线程重新进行调度。
Windows98Windows98没有配备该函数的非常有用的实现代码。
7.5线程的运行时间
有时想要计算线程执行某个任务需要多长的时间。
许多人采取的办法是编写类似下面的代码:
//Getthecurrenttime(starttime).
DWORDdwStartTime=GetTickCount();
//Performcomplexalgorithmhere.
//Subtractstarttimefromcurrenttimetogetduration.
DWORDdwElapsedTime=GetTickCount()-dwStartTime;
这个代码做了一个简单的假设:
即它不会被中断。
但是,在抢占式操作系统中,永远无法知道线程何时被赋予CPU时间。
当取消线程的CPU时间时,就更难计算线程执行不同任务时所用的时间。
我们需要一个函数,以便返回线程得到的CPU时间的数量。
幸运的是,Windows提供了一个称为GetThreadTimes的函数,它能返回这些信息:
BOOLGetThreadTimes(HANDLEhThread,
PFILETIMEpftCreationTime,PFILETIMEpftExitTime,
PFILETIMEpftKernelTime,PFILETIMEpftUserTime);
GetThreadTimes函数返回4个不同的时间值,这些值如表7-1所示。
表7-1GetThreadTimes函数的返回时间值
时间值
含义
创建时间
用英国格林威治时间1601年1月1日午夜后100ns的时间间隔表示的英国绝对值,用于指明线程创建的时间
退出时间
用英国格林威治时间1601年1月1日午夜后100ns的时间间隔表示的英国绝对值,用于指明线程退出的时间。
如果线程仍然在运行,退出时间则未定义
内核时间
一个相对值,用于指明线程执行操作系统代码已经经过了多少个100ns的CPU时间
用户时间
一个相对值,用于指明线程执行应用程序代码已经经过了多少个100ns的CPU时间
使用这个函数,可以通过使用下面的代码确定执行复杂的算法时需要的时间量:
__int64FileTimeToQuadWord(PFILETIMEpft)
{
return(Int64ShllMod32(
pft->dwHighDateTime,32)|pft->dwLowDateTime);
}
voidPerformLongOperation()
{
FILETIMEftKernelTimeStart,ftKernelTimeEnd;
FILETIMEftUserTimeStart,ftUserTimeEnd;
FILETIMEftDummy;
__int64qwKernelTimeElapsed,qwUserTimeElapsed,
qwTotalTimeElapsed;
//Getstartingtimes.
GetThreadTimes(GetCurrentThread(),&ftDummy,
&ftDummy,&ftKernelTimeStart,&ftUserTimeStart);
//Performcomplexalgorithmhere.
//Getendingtimes.
GetThreadTimes(GetCurrentThread(),&ftDummy,
&ftDummy,&ftKernelTimeEnd,&ftUserTimeEnd);
//Gettheelapsedkernelandusertimesby
//convertingthestartandendtimes
//fromFILETIMEstoquadwords,andthen
//subtractthestarttimesfromtheendtimes.
qwKernelTimeElapsed=
FileTimeToQuadWord(&ftKernelTimeEnd)-
FileTimeToQuadWord(&ftKernelTimeStart);
qwUserTimeElapsed=
FileTimeToQuadWord(&ftUserTimeEnd)-
FileTimeToQuadWord(&ftUserTimeStart);
//Gettotaltimedurationbyaddingthekernel
//andusertimes.
qwTotalTimeElapsed=qwKernelTimeElapsed+
qwUserTimeElapsed;
//Thetotalelapsedtimeisin
//qwTotalTimeElapsed.
}
注意,GetProcessTimes是个类似GetThreadTimes的函数,适用于进程中的所有线程:
BOOLGetProcessTimes(HANDLEhProcess,
PFILETIMEpftCreationTime,PFILETIMEpftExitTime,
PFILETIMEpftKernelTime,PFILETIMEpftUserTime);
GetProcessTimes返回的时间适用于某个进程中的所有线程(甚至是已经终止运行的线程)。
例如,返回的内核时间是所有进程的线程在内核代码中经过的全部时间的总和。
Windows98遗憾的是,GetThreadTimes和GetProcessTimes这两个函数在Windows98中不起作用。
在Windows98中,没有一个可靠的机制可供应用程序来确定线程或进程已经使用了多少CPU时间。
对于高分辨率的配置文件来说,GetThreadTimes并不完美。
Windows确实提供了一些高分辨率性能函数:
BOOLQueryPerformanceFrequency(
LARGE_INTEGER*pliFrequency);
BOOLQueryPerformanceCounter(
LARGE_INTEGER*pliCount);
虽然这些函数认为,正在执行的线程并没有得到抢占的机会,但是高分辨率的配置文件是为短期存在的代码块设置的。
为了使这些函数运行起来更加容易一些,我创建了下面这个C++类:
classCStopwatch
{
public:
CStopwatch()
{
QueryPerformanceFrequency(&m_liPerfFreq);
Start();
}
voidStart()
{
QueryPerformanceCounter(&m_liPerfStart);
}
__int64Now()const
{
//Returns#ofmillisecondssince
//Startwascalled
LARGE_INTEGERliPerfNow;
QueryPerformanceCounter(&liPerfNow);
return(((liPerfNow.QuadPart-
m_liPerfStart.QuadPart)*1000)/
m_liPerfFreq.QuadPart);
}
private:
//Countspersecond
LARGE_INTEGERm_liPerfFreq;
//Startingcount
LARGE_INTEGERm_liPerfStart;
};
使用这个类如下:
//C