第七章 线程调度,优先级和关联性
本章内容
7.1 线程的挂起和恢复
7.2 进程的挂起和恢复
7.3 睡眠
7.4 切换到另一个线程
7.5 在超线程CPU上切换到另一个线程
7.6 线程的执行时间
7.7 在实际上下文中谈CONTEXT结构
7.8 线程优先级
7.9 从抽象角度看优先级
7.10 优先级编程
7.11 关联性
CONTEXT反应了线程上一次执行时CPU寄存器的状态。 大约每间隔20ms (GetSystemTimeAdjustment函数的第二个参数返回值),windows都会查看当前存在的线程内核对象。在这些对象中,只有一些被认为是可调度的。Windows在可调度的线程内核对象中选择一个,并将上次保存的CONTEX载入寄存器。
这一操作被称为上下文切换(CONTEXT switch)。 windows会记录每个线程运行的次数。
例如notepad的线程已经被调度了62471次
图中的线程正在执行代码,并在进程的地址空间中操作数据。
又过了大约20ms,windows将cpu寄存器存回线程的上下文,线程不再运行。系统再次检查剩下可调度的线程内核对象,选择另一个线程的内核对象,载入CONTEXT到寄存器,然后继续。
Windows之所以称为抢占式多线程操作系统(preemptive multithreaded operating system)系统可以在任何时候停止一个线程而另行调度另一个线程。
Windows只调度可调度线程,如果线程挂起计数器大于0,这意味改线程已经被挂起,不应该给其调度任何CPU时间。
还有很多线程在等待事件。
7.1 线程的挂起和恢复
线程内核对象中有一个值表示挂起计数。用CreateProcess或CreateThread创建线程时,默认挂起计数为1.然后执行线程初始化工作。
线程初始化完成以后,CreateProcess或CreateThread会检查是否有CREATE_SUSPENDED标志传入。如果有会重新让线程处于挂起状态。否则将挂起计数器设置为0.线程成为可调度。
可以通过ResumeThread 用于递减线程的挂起计数器。该函数返回线程内核对象的前一个挂起计数。如果线程是非挂起状态,则返回0xFFFFFFFF
DWORD SuspendThread(HANDLE hThread);可以用于挂起线程。
可以挂起自身和别的线程。返回之前的线程挂起计数。 一个线程最多可以挂起MAXIMUM_SUSPEND_COUNT次数(winnt.h定义为127)。
实际开发中,随时调用SuspendThread必须小心,如果目标线程在分配堆内存,线程将锁定堆。将其挂起将导致其他线程不可访问堆。必须等其恢复。
所以必须明确知道目标线程在做什么采取完备的措施才可将其挂起。
7.2 进程的挂起和恢复
其实Windows中并不存在进程的挂起和恢复的概念,因为windows 从来不会给进程调度cpu时间。有时候需要挂起进程中的所有线程例如调式代码。
调试器处理WaitForDebugEvent返回的调试事件时,windows将挂起目标进程的所有线程。直到调试器调用ContinueDebugEvent。
也可以使用Process Explorer的Suspend Process来挂起目标进程中的所有线程。
作者自己实现的SuspendProcess函数
VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) {
// Get the list of threads in the system.
HANDLE hSnapshot = CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD, dwProcessID);
if (hSnapshot != INVALID_HANDLE_VALUE) {
// Walk the list of threads.
THREADENTRY32 te = { sizeof(te) };
BOOL fOk = Thread32First(hSnapshot, &te);
for (; fOk; fOk = Thread32Next(hSnapshot, &te)) {
// Is this thread in the desired process?
if (te.th32OwnerProcessID == dwProcessID) {
// Attempt to convert the thread ID into a handle.
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,
FALSE, te.th32ThreadID);
if (hThread != NULL) {
// Suspend or resume thre thread.
if (fSuspend)
SuspendThread(hThread);
else
ResumeThread(hThread);
}
CloseHandle(hThread);
}
}
CloseHandle(hSnapshot);
}
}
利用Toolhelp32函数来枚举系统中的线程列表。找到属于某个进程的线程,使用
OpenThread来打开该线程的内核对象句柄,
SuspendProcess不能随时调用,因为在枚举线程集合的时候,会有新线程被创建,也可能有线程被销毁。这样可能目标进程创建的新线程不会被挂起。
在调用SuspendThread的时候可会恢复一个未挂起的线程。更糟糕的是在枚举线程ID时,可能会销毁一个已有的线程,创建一个新的线程,而这两个线程恰好ID相同。这样的话函数将挂起任意一个线程。所以在使用前谨慎了解目标进程的运行情况。
7.3 睡眠
线程还可以告知系统一段时间内自己不需要调度了。可以通过Sleep实现。
VOID Sleep(DWORD dwMilliseconds);
将使线程挂起自己dwMilliseconds长的时间。
1)线程自愿放弃属于它的时间片中剩下的部分
2)系统设置的不可调度时间只是近似于所设定的毫秒数。
3)可以调用Sleep并传递INFINITE,告知系统永远不要调度这个线程。(没意义,不如直接退出进程释放系统资源)
4)Sleep 0 告知系统放弃当前的时间片的剩余部分,强制调度其他线程。
7.4 切换到另一个线程
BOOL SwitchToThread();
允许调用一个低优先级的线程。然后返回。和Sleep(0)类似,但Sleep 不会允许执行低优先级线程(Win2K以后无此限定)。
7.5 在超线程CPU上切换到另一个线程
(2)虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。
(3)当一个线程中止时,CPU自动执行另一个线程,无需操作系统干预。
(4)在超线程CPU上执行一个旋转循环(spin loop)代码时,我们需要强制当前线程暂停,从而另一个线程可以使用芯片资源。在Win32API中,可以调用YieldProcessor宏来强制让出CPU。
7.6 线程的执行时间
有时候需要计算一个线程执行某些任务需要消耗多长时间。
ULONGLONG qwStartTime = GetTickCount64;
ULONGLONG qwElapsedTime = GetTickCount64() - qwStartTime;
这个代码有一个前提,代码执行不会被中断。
可以采用以下函数
1. GetThreadTimes 精度毫秒级别
WINBASEAPI
BOOL
WINAPI
GetThreadTimes(
_In_ HANDLE hThread,
_Out_ LPFILETIME lpCreationTime,
_Out_ LPFILETIME lpExitTime,
_Out_ LPFILETIME lpKernelTime,
_Out_ LPFILETIME lpUserTime
);
例子,使用该函数确定一个复杂算法所需的时间。
__int64 FileTimeToQuadWord(PFILETIME pft) {
return(Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime);
}
void PerformanLongOperation() {
FILETIME ftKernelTimeStart, ftKernelTimeEnd;
FILETIME ftUserTimeStart, ftUserTimeEnd;
FILETIME ftDummy;
__int64 qwKernelTimeElapsed, qwUserTimeElapsed,
qwTotalTimeElapsed;
// Get starting times.
GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
&ftKernelTimeStart, &ftUserTimeStart);
// Perform comlex algorithm here.
for (int i = 0; i < 100000; i++)
{
for (int j = 0; j < 100; j++)
double s = pow(10.0, log10(sqrt(sqrt(99.0) * sqrt(99.0))));
}
// Get ending times.
GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
&ftKernelTimeEnd, &ftUserTimeEnd);
// Get the elapsed kernel and user times by converting the start
// and end times from FILETIMES to quad words, and then subtract
// the start times from the end times.
qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) -
FileTimeToQuadWord(&ftKernelTimeStart);
qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) -
FileTimeToQuadWord(&ftUserTimeStart);
// Get total time duration by adding the kernel and user times.
qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;
// The total elapsed time is in qwTotalTimeElapsed.
}
在Vista以上系统不再依赖10-·15ms间隔的始终计时器来为线程分配cpu时间,而是采用64位时间截计时器(Time Stamp Counter,TSC)
QueryThreadCycleTime 和 QueryProcessCycleTime 返回指定线程和指定进程中所有线程所使用的时钟周期数。
如果想更精确代替GetTickCount64, 使用ReadTimeStampCounter宏获取当前TSC值。
要进行高精度的性能分析GetThreadTimes函数仍然不够。采用以下函数
2. ReadTimeStampCounter (执行rdtsc指令)返回CPU自上一次reset状态以来所运行的的周期数。纳秒级别的精度
MSDN
Generates the rdtsc
instruction, which returns the processor time stamp. The processor time stamp records the number of clock cycles since the last reset.
3. 硬件级别的高精度计时器通常精度可达微妙级别
B