《Windows核心编程》读书笔记七 线程调度,优先级和关联性

来源:互联网 发布:xp打开1433端口 编辑:程序博客网 时间:2024/05/22 03:41

第七章 线程调度,优先级和关联性

本章内容

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 线程的挂起和恢复

线程内核对象中有一个值表示挂起计数。用CreateProcessCreateThread创建线程时,默认挂起计数为1.然后执行线程初始化工作。

线程初始化完成以后,CreateProcessCreateThread会检查是否有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);elseResumeThread(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上切换到另一个线程


(1)超线程处理器芯片上有多个“逻辑CPU”,每个都可以运行一个线程(注意,与多核CPU不同!)。这样的处理器一般需要多加入一个Logical CPU Pointer(逻辑处理单元),让每个线程都有自己的状态寄存器。而其余部分如ALU(整数运算单元)、FPU(浮点运算单元)、L2 Cache(二级缓存)则保持不变,这些部分是被共享的。


(2)虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。


(3)当一个线程中止时,CPU自动执行另一个线程,无需操作系统干预。


(4)在超线程CPU上执行一个旋转循环(spin loop)代码时,我们需要强制当前线程暂停,从而另一个线程可以使用芯片资源。在Win32API中,可以调用YieldProcessor宏来强制让出CPU。


7.6 线程的执行时间

有时候需要计算一个线程执行某些任务需要消耗多长时间。

ULONGLONG qwStartTime = GetTickCount64;

ULONGLONG qwElapsedTime = GetTickCount64() - qwStartTime;

这个代码有一个前提,代码执行不会被中断。


可以采用以下函数

1. GetThreadTimes 精度毫秒级别

WINBASEAPIBOOLWINAPIGetThreadTimes(    _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.}


GetProcessTimes 类似GetThreadTimes 用于进程中的所有线程。该函数统计了进程中的所有线程的使用时间总和。

在Vista以上系统不再依赖10-·15ms间隔的始终计时器来为线程分配cpu时间,而是采用64位时间截计时器(Time Stamp Counter,TSC)

QueryThreadCycleTimeQueryProcessCycleTime 返回指定线程和指定进程中所有线程所使用的时钟周期数。

如果想更精确代替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. 硬件级别的高精度计时器通常精度可达微妙级别

BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency); 高精度计时器的运行频率

BOOL QueryPerformanceCounter(LAGER_INTEGER* pliCount); 高精度计时器的运行计数器

精确获取时间:QueryPerformanceFrequency() - 基本介绍类型:Win32API原型:BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency);作用:返回硬件支持的高精度计数器的频率。返回值:非零,硬件支持高精度计数器;零,硬件不支持,读取失败。


BOOL QueryPerformanceCounter(LARGE_INTEGER *ipPerformanceCount);//参数指向计数器的值参数LARGE_INTEGER *ipProformanceCount为一个指针变量用于函数传值,即指向现时计数器的值.如果安装的硬件不支持高精度计时器,该参数将返回0,关于返回值:如果安装的硬件支持高精度计时器,函数将返回非0值.如果安装的硬件不支持高精度计时器,函数将返回0.)


作者创建了一个类进行统计分析

class CStopwatch {public:CStopwatch() { QueryPerformanceFrequency(&m_liPerfFreq); Start(); }void Start() { QueryPerformanceCounter(&m_liPerfStart); }__int64 Now() const { // Returns # of ms since Start was calledLARGE_INTEGER liPerfNow;QueryPerformanceCounter(&liPerfNow);return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000)/ m_liPerfFreq.QuadPart);}__int64 NowInMicro() const { // Return # of microseconds// since Start was calledLARGE_INTEGER liPerfNow;QueryPerformanceCounter(&liPerfNow);return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000000)/ m_liPerfFreq.QuadPart);}private:LARGE_INTEGER m_liPerfFreq;// Counts per secondLARGE_INTEGER m_liPerfStart; // Starting count};

用法如下

// Create a stopwatch timer (which defaults to the current time).CStopwatch stopwatch;// Execute the code I want to profile here.// Get how much time has elapsed up to now.__int64 qwElapsedTime = stopwatch.Now();// awElapsedTime indicates how long the profiled code// executed in ms.


GetCPUFrequencyInMHz() 获取当前cpu的时钟频率

算法总体思路

1)调用SetThreadPriority将线程优先级提到THREAD_PRIORITY_HIGHEST以获取尽可能多的时间片

2)使用QueryPerformanceFrequency和QueryPerformanceCounter的组合用高精度计时器来获取经过的时间段。

3)使用ReadTimeStampCounter宏来获取在步骤2)所统计的时间截内CPU所运行的周期(tick count)

4)将步骤3的周期数/步骤2的时间截 算出来的就是单位时间内CPU的时钟频率

DWORD GetCPUFrequencyInMHz() {// change the priority to ensure the thread will have more chances// to be scheduled when Sleep() endsint currentPriority = GetThreadPriority(GetCurrentThread());SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);// keep track of elapsed time with the other timer__int64 elapsedTime = 0;// Create a stopwatch timer (which defaults to the current time).CStopwatch stopwatch;__int64 perfCountStart = stopwatch.NowInMicro();// get the current number of cyclesunsigned __int64 cyclesOnStart = ReadTimeStampCounter();// wait for ~1 secondSleep(1000);// get the number of cycles after ~1 secondunsigned __int64 numberOfCycles = ReadTimeStampCounter() - cyclesOnStart;// Get how much time has elapsed with greater precisionelapsedTime = stopwatch.NowInMicro() - perfCountStart;// Restore the thread priotitySetThreadPriority(GetCurrentThread(), currentPriority);// Compute the frequency in MHzDWORD dwCPUFrequency = (DWORD(numberOfCycles / elapsedTime));return (dwCPUFrequency);}


该函数统计了给线程分配的当前CPU的时钟频率。如果在多CPU或者开启了节能模式的CPU上可能不一定准确。仅供参考。

该种方法的缺点:精度虽高,但数据抖动比较厉害,在节能模式的时候结果偏慢,超频模式的时候又偏快,而且用电池和接电源的时候效果也不一样。此外用这种方法求时间还得考虑用SetThreadPriority提高优先级尽量独占时间片。并使用SetThreadAffinityMask以确保每次调用QueryPerformanceCounter的时候在同一CPU核心上。


高精度定时器在和精确时间获取在某些科学计算领域,通讯,视频数据采样,时间同步,媒体流平滑控制,拥塞算法等等都需要提供毫秒级别的精度。而Windows设计之初并非是以实时系统来考虑的,精度一般在15ms左右。受影响的函数包括,Sleep, GetTickCount, _ftime等。对高精度系统来说就非常致命。

扩展阅读,参考此文。介绍高精度定时器的实现和精确时刻获取。

http://blog.csdn.net/chenlycly/article/details/16479519


7.7 在实际上下文中谈CONTEXT结构

CONTEXT记录了CPU的各种寄存器状态,以方便线程可以从停止状态继续运行。但CONTEXT是CPU相关的。

Winnt.h中定义的CONTEXT结构

//// Context Frame////  This frame has a several purposes: 1) it is used as an argument to//  NtContinue, 2) is is used to constuct a call frame for APC delivery,//  and 3) it is used in the user level thread creation routines.////  The layout of the record conforms to a standard call frame.//typedef struct _CONTEXT {    //    // The flags values within this flag control the contents of    // a CONTEXT record.    //    // If the context record is used as an input parameter, then    // for each portion of the context record controlled by a flag    // whose value is set, it is assumed that that portion of the    // context record contains valid context. If the context record    // is being used to modify a threads context, then only that    // portion of the threads context will be modified.    //    // If the context record is used as an IN OUT parameter to capture    // the context of a thread, then only those portions of the thread's    // context corresponding to set flags will be returned.    //    // The context record is never used as an OUT only parameter.    //    DWORD ContextFlags;    //    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT    // included in CONTEXT_FULL.    //    DWORD   Dr0;    DWORD   Dr1;    DWORD   Dr2;    DWORD   Dr3;    DWORD   Dr6;    DWORD   Dr7;    //    // This section is specified/returned if the    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.    //    FLOATING_SAVE_AREA FloatSave;    //    // This section is specified/returned if the    // ContextFlags word contians the flag CONTEXT_SEGMENTS.    //    DWORD   SegGs;    DWORD   SegFs;    DWORD   SegEs;    DWORD   SegDs;    //    // This section is specified/returned if the    // ContextFlags word contians the flag CONTEXT_INTEGER.    //    DWORD   Edi;    DWORD   Esi;    DWORD   Ebx;    DWORD   Edx;    DWORD   Ecx;    DWORD   Eax;    //    // This section is specified/returned if the    // ContextFlags word contians the flag CONTEXT_CONTROL.    //    DWORD   Ebp;    DWORD   Eip;    DWORD   SegCs;              // MUST BE SANITIZED    DWORD   EFlags;             // MUST BE SANITIZED    DWORD   Esp;    DWORD   SegSs;    //    // This section is specified/returned if the ContextFlags word    // contains the flag CONTEXT_EXTENDED_REGISTERS.    // The format and contexts are processor specific    //    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];} CONTEXT;

CONTEXT_CONTROL包含CPU的控制寄存器,比如Eip(指令指针),Esp(栈指针),Eflags(标志寄存器)和函数返回地址

CONTEXT_INTEGER标识CPU的整数寄存器EAX, EBX, ECX, EDX, ESI, EDI

CONTEXT_SEGMENTS标识CPU的段寄存器ES,DS,FS,GS  (FS,GS是80386新增加的两个段寄存器,辅助使用以减轻ES段寄存器的压力)

CONTEXT_DEBUG_REGISTERS表示CPU的调试寄存器

CONTEXT_EXTENDED_REGISTERS标识CPU的扩展寄存器

Windows允许我们查看线程内核对象内的CONTEXT数据结构。

WINBASEAPIBOOLWINAPIGetThreadContext(    _In_ HANDLE hThread,    _Inout_ LPCONTEXT lpContext    );

先分配一个CONTEXT结构的变量,初始化一些标志ContextFlags成员 以表示要获取哪些寄存器,再将其地址传递给GetThreadContext。

另外在获取CONTEXT之前需要先SuspendThread先将目标线程挂起。否则若目标线程正好被CPU调度则获取的CONTEXT和实际情况就不一致了。

系统允许一个线程有两个CONTEXT,用户模式和内核模式。GetThreadContext只允许访问用户模式的CONTEXT

因此,若目标线程正在内核模式下运行(比如malloc函数进入内核申请堆,并不会响应SuspendThread),调用了SuspendThread将其挂起,实际上线程还没有暂停。但是他不会再执行用户模式的代码。我们可以认为线程已经暂停,这是调用GetThreadContext是非常安全的。


ContextFlags成员仅仅告诉GetThreadContext函数要获取哪些寄存器。例如要获取控制寄存器

// Create a CONTEXT structure.CONTEXT Context = { 0 };// Tell the system that we are interested in only the // control registers.Context.ContextFlags = CONTEXT_CONTROL;// Tell the system to get the registers associated with a thread.GetThreadContext(hThread, &Context); // hThread is the target thread kernel object.// Thre control register members in the CONTEXT structure// reflect the thread's control registers. The other members// are undefined.

如果想获取线程的控制寄存器和整数寄存器,可以这样初始化

// Tell the system that we are interested in only the // control registers.Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;

获取所有寄存器

// Tell the system that we are interested in only the // control registers.Context.ContextFlags = CONTEXT_FULL;

Windows还允许修改的线程的CONTEXT

WINBASEAPIBOOLWINAPISetThreadContext(    _In_ HANDLE hThread,    _In_ CONST CONTEXT * lpContext    );
同样在执行之前需要先SuspendThread目标线程。否则会引起不可预料的后果


// Create a CONTEXT structure.CONTEXT Context = { 0 };// Stop the thread from running.SuspendThread(hThread);// Tell the system that we are interested in only the // control registers.Context.ContextFlags = CONTEXT_CONTROL;// Tell the system to get the registers associated with a thread.GetThreadContext(hThread, &Context); // hThread is the target thread kernel object.// Thre control register members in the CONTEXT structure// reflect the thread's control registers. The other members// are undefined.// Make the instruction pointer point to the address of your choice.// Here I've arbitrarily set the address instruction pointer to// )x00010000.Context.Eip = 0x00010000;// Set the thread's register to reflect the changed values.// It's not really necessary to reset the ContextFlags member// because it was set eralier.Context.ContextFlags = CONTEXT_CONTROL;SetThreadContext(hThread, &Context);// Resuming the thread will cause it to begin executiong// at address )x0001000.ResumeThread(hThread);


这可能使远程线程访问违规从而导致远程进程被终止。


通常这些寄存器的获取和设定用于调试器。


7.8 线程优先级

在调度程序给一个可调度线程分配CPU之前,CPU可以运行一个线程大约20ms。这是优先级都相同的情况,实际上,各个线程有很多不同的优先级,这将影响调度程序如何选择下一个要运行的线程。


1)每个线程被赋予0(最低)~31(最高)的优先级数。

2)CPU首先查看优先级最高的线程,并以循环(round-robin)的方式进行调度。一个31优先级的结束,cpu会调度领一个优先级为31的线程。

3)只要有优先级为31的线程,系统就不会给优先级0-30的线程分配CPU,称为饥饿(starvation)在多处理器系统上饥饿发生的可能性要小得多。

4)较高优先级的线程会抢占较低优先级线程的时间片,例如一个优先级的5线程正在执行,系统确定有一个更高优先级的线程准备运行,会立即暂停较低优先级的线程(即使他还有时间片没用完)并将cpu分配给较高优先级的线程,该线程将获得一个完整的时间片。

系统启动时会创建一个0优先级的页面清0线程(zero page thread)负责在系统空闲时将内地中所有闲置页面清零


7.9 从抽象角度看优先级

微软并未公开调度算法完整描述调度程序的行为。也不允许应用程序充分利用调度程序的特性。并明确告知用户调度算法会发生变化。

微软在调度程序之上进行了一层抽象提供了一系列API,用户无法直接接触到底层的调度程序。


Windows支持6个优先级类(priority class):idle, below normal, normal, above normal, high 和 real-time


idle优先级类非常适合只在系统什么都不做的时候运行的程序。例如屏幕保护程序。

real-time 一般是执行延迟非常低的短任务-例如需要响应硬件事件。

进程一般情况不能运行在Real-time优先级类下,除非用户有Increase Scheduling Priority特权。

Win2K以上又增加了below normal和above normal

有了优先级类就可以不用关注和其他应用程序的关系了,转而关注程序里的线程。

Windows有7个相对的线程优先级:idle, lowest, below normal, normal, above normal, highest 和 time-critical。这些是相对于进程优先级而言。

大多数线程使用normal线程优先级。

相对线程优先级


也就是进程都属于某个优先级类,另外可以指定进程中线程的相对优先级。

Vista系统上的进程优先级和相对线程优先级与优先级值的映射。



优先级0 预留给页面清零线程了,系统不允许其他任何线程的优先级为0.

还有一些优先级应用程序也无法获得,17~30.预留给内核模式的设备驱动程序。用户模式的应用程序是不能获得这些优先级的。

Real-time优先级类的进程的线程的优先级不能低于16, 非Real-time进程的线程优先级也不可高于15.


补充说明:



7.10 优先级编程

CreateProcess时候fdwCreate参数可以传入优先级。


父进程可以替想要创建的子进程选择其优先级类。子进程被创建以后可以通过SetPriorityClass来修改自己的优先级类。

WINBASEAPIBOOLWINAPISetPriorityClass(    _In_ HANDLE hProcess,    _In_ DWORD dwPriorityClass    );
PriorityClass的值见上表。


例如修改当前线程所属的进程的优先级类为idle

SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);

获取进程优先级的函数

WINBASEAPIDWORDWINAPIGetPriorityClass(    _In_ HANDLE hProcess    );

命令行默认启动的进程为Normal优先级。可以使用Start命令设定启动优先级

C:\>START /LOW CALC.EXE

还可以设定/BELOWNORMAL, /NORMAL, /ABOVENORMAL, /HIGH /REALTIME

也可以通过任务管理器修改进程的优先级类

CreateThread创建的线程初始线程优先级为Normal, 。使用以下函数可以改变线程相对优先级

WINBASEAPIBOOLWINAPISetThreadPriority(    _In_ HANDLE hThread,    _In_ int nPriority    );

nPriority参数如下表


获取线程优先级

WINBASEAPIintWINAPIGetThreadPriority(    _In_ HANDLE hThread    );

例如创建一个线程优先级为idle的线程。

DWORD dwThreadID;HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL,CREATE_SUSPENDED, &dwThreadID);SetThreadPriority(hThread, THREAD_PRIORITY_IDLE);ResumeThread(hThread);CloseHandle(hThread);

创建线程时设置为CREATE_SUSPENDED先将其挂起

然后设置线程优先级为THREAD_PRIORITY_IDLE

ResumeThread恢复线程运行

关闭线程的内核对象句柄

注意:微软并未提供返回线程优先级的函数(0~31数值)。因为微软保留线程调度算法随时可能改变的权利。


7.10.1 动态提升线程优先级

系统通过线程的“”相对优先级“”和线程所属进程的“”优先级类“”来确定其优先级值(基本优先级值 base priority level)。 偶尔系统也会提升一个线程的优先级值-为了响应某种IO或窗口消息或磁盘读取。


书上讲了一个例子,例如一个基础优先级13的线程收到WM_KEYDOWN后被体统提升为15,处理完一个时间片以后,递减至14,最后到13保持不变。

也就是线程的当前优先级不会低于其基本优先级。但是微软没公开设备驱动能将线程优先级动态提升多少。系统只能提升优先级在(1~15 ,不会高于15)

系统不会动态提高范围(16~31)的线程。

可以禁止系统动态提升线程优先级

WINBASEAPIBOOLWINAPISetProcessPriorityBoost(    _In_ HANDLE hProcess,    _In_ BOOL bDisablePriorityBoost    );

禁止或开启系统对一个进程中的所有线程动态提升优先级



WINBASEAPIBOOLWINAPISetThreadPriorityBoost(    _In_ HANDLE hThread,    _In_ BOOL bDisablePriorityBoost    );
禁止或开启系统对一个线程的动态提升优先级


还可以获取当前是否已经开启了动态提升优先级功能。

WINBASEAPIBOOLWINAPIGetProcessPriorityBoost(    _In_ HANDLE hProcess,    _Out_ PBOOL pDisablePriorityBoost    );WINBASEAPIBOOLWINAPIGetThreadPriorityBoost(    _In_ HANDLE hThread,    _Out_ PBOOL pDisablePriorityBoost    );

还有一种情况系统也会动态提升线程的优先级。例如一个优先级为4的线程,但又有一个优先级为8的线程一直处于调度状态。

因此4优先级的线程无法被调度,在这种情况下优先级为4的线程处于CPU时间饥饿状态。当系统检测到线程已经处于饥饿状态3-4秒,它会动态将饥饿状态的优先级提升为15,并允许线程运行两个时间片。当两个时间片结束,线程会立即回复到基本优先级。


7.10.2 为前台进程微调调度程序

用户需要使用某个进程窗口(称为前台进程foreground process) 而所有其他的进程成为后台进程(background process)。

前台进程处于Normal优先级下,windows会微调前台进程的调度算法。系统给前台进程的线程分配比一般情况下更多的时间片。

系统属性->高级->性能计划 中可以进行微调



7.10.3 调度I/O请求优先级

低优先级进程可以在一个时间片写入成百上千的IO请求入入列。会显著的影响高优先级线程的性能。(例如后台磁盘碎片整理,病毒扫描,内容索引程序)

windows VISTA开始线程可以在进行I/O请求时设置优先级了。

设置SetThreadPriority 并传入 THREAD_MODE_BACKGROUND_BEGIN来设置其IO请求优先级。这里是低优先级IO请求,同时会降低其CPU调度优先级

也可以恢复THREAD_MODE_BACKGROUND_END 默认的优先级IO请求,恢复normal cpu调度优先级。

这里的hThread必须是GeCurrentThread 也就是当前线程自己才可以修改其IO优先级。不允许修改另一个线程的IO优先级。


如果要让进程中所有线程都是用低IO请求和低CPU调度。可以使用

SetPriorityClass 并传入 PROCESS_MODE_BACKGROUND_BEGIN 同时降低当前进程所有线程的IO请求优先级和cpu调度优先级。

相反传入PROCESS_MODE_BACKGROUND_END 表示恢复默认IO请求优先级和cpu调度优先级。

这里的进程句柄只能是当前进程自己GetCurrentProcess()不可以修改别的进程的IO优先级。


在更细的粒度上normal优先级线程还可以执行对某个文件执行后台优先级I/O,例如

FILE_IO_PRIORITY_HINT_INFO phi;phi.PriorityHint = IoPriorityHintLow;SetFileInformationByHandle(hFile, FileIoPriorityHintInfo, &phi, sizeof(FILE_IO_PRIORITY_HINT_INFO));

SetFileInformationByHandle设置的优先级将覆盖SetPriorityClass或者SetThreadPriority设置的优先级。

应该尽量避免优先级逆转。

优先级倒置,又称优先级反转优先级逆转优先级翻转,是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。
这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权

7.10.4 Scheduling Lab 示例程序


笔者开了一个实例发现cpu使用率在25%稳定

开了4个实例cpu使用率实际只有66% 并不能使用完全部4核CPU的资源(笔者用的是4核心的i5-3470 CPU)

再开了8个实例以后CPU的使用率达到了70%~84% 还是不能使用完全部的CPU资源,并且系统并无明显的卡顿感

将8个实例全部开到Priority Class High

Thread relative priority:Time Critical

还是依旧无卡顿感。。。 Win7系统的多线程用户体验优化还是可以的。

发现及时改成Priority Class IDLE 

Thread relative priority: IDLE

数字的更新依旧非常连贯。 Win7修改了调度策略???

唯一影响更新时间的就是Sleep。 这个一修改明显感觉更新变慢了。而且在sleep值4位以后对EditBox的修改响应也明显变慢。


Win7 系统主窗口不响应WM_PAINT以后仍然有UI显示。(xp下直接反白,估计是HDC被清空了)

可能在单核系统下这个实验会比较有代表性(后续在虚拟机下实验一下)

多核系统下为了用户体验Win7明显在响应UI和系统应用方面的线程调度算法做了很大的优化。

右边的List更新速度较快只要不设置sleep基本都非常连贯的更新, 目测需要写一个统计算法来计算Performance。

为了便于观察性能笔者自行修改了一下代码。加入一个统计性能的Edit每1秒更新一次计数。可以看到不同优先级下性能的差异。


但是实际测试发现Below normal 的性能反而高于Normal。 IDLE和Normal差异很低

最后测试了开启5个窗口实例。4个运行于high-Time Critical 一个运行于IDLE-IDLE


4个High -Time Critical 的实际性能比较接近

IDLE-IDLE的性能略低,但是并没有达到不可调度的程度(看来windows对于低优先级的CPU饥饿进程还是比较照顾的)


关于Sleep的影响在加入粗略性能统计之前就有直观的感受了,这里仅仅设置为sleep 1ms就可以看到明显的性能下降



最后贴上略微修改过的代码。

SchedLab.cpp

/*Module:SchedLab.cppNotices:Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre*/#include "..\CommonFiles\CmnHdr.h"#include <windowsx.h>#include <tchar.h>#include <strsafe.h>#include "Resource.h"//////////////////////////////////////////////////////////////////////////class CStopwatch {public:CStopwatch() { QueryPerformanceFrequency(&m_liPerfFreq); Start(); }void Start() { QueryPerformanceCounter(&m_liPerfStart); }__int64 Now() const { // Returns # of millisecond since Start was calledLARGE_INTEGER liPerfNow;QueryPerformanceCounter(&liPerfNow);return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000)/ m_liPerfFreq.QuadPart);}__int64 NowInMicro() const { // Return # of microseconds// since Start was calledLARGE_INTEGER liPerfNow;QueryPerformanceCounter(&liPerfNow);return(((liPerfNow.QuadPart - m_liPerfStart.QuadPart) * 1000000)/ m_liPerfFreq.QuadPart);}private:LARGE_INTEGER m_liPerfFreq;// Counts per secondLARGE_INTEGER m_liPerfStart; // Starting count};__int64 lastNumCount = 0;__int64 currentCount = 0;__int64 lastTimeStart = 0;CStopwatch stopWath;//////////////////////////////////////////////////////////////////////////DWORD WINAPI ThreadFunc(PVOID pvParam) {HANDLE hThreadPrimary = (HANDLE)pvParam;SuspendThread(hThreadPrimary);chMB("The Primary thread is suspended.\n""It no longer responds to input and produces no output.\n""Press OK to resume the primary thread & exit this secondary thread.\n");ResumeThread(hThreadPrimary);CloseHandle(hThreadPrimary);// To avoid deadlock, call EnableWindow after ResumeThreads.EnableWindow(GetDlgItem(FindWindow(NULL, TEXT("Scheduling Lab")), IDC_SUSPEND),TRUE);return(0);}//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_SCHEDLAB);// Initialize process priority classesHWND hWndCtl = GetDlgItem(hWnd, IDC_PROCESSPRIORITYCLASS);int n = ComboBox_AddString(hWndCtl, TEXT("High"));ComboBox_SetItemData(hWndCtl, n, HIGH_PRIORITY_CLASS);// Save our current priority classDWORD dwpc = GetPriorityClass(GetCurrentProcess());if (SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS)) {// This system supports the BELOW_NOAMAL_PRIORITY_CLASS class// Restore our original priority classSetPriorityClass(GetCurrentProcess(), dwpc);// Add the Above Normal priority classn = ComboBox_AddString(hWndCtl, TEXT("Above normal"));ComboBox_SetItemData(hWndCtl, n, ABOVE_NORMAL_PRIORITY_CLASS);dwpc = 0; // Remember that this system supports below normal}int nNormal = n = ComboBox_AddString(hWndCtl, TEXT("Normal"));ComboBox_SetItemData(hWndCtl, n, NORMAL_PRIORITY_CLASS);if (dwpc == 0) {// This system supports the BELOW_NORMAL_PRIORITY_CLASS class// Add the Below Normal priority classn = ComboBox_AddString(hWndCtl, TEXT("Below normal"));ComboBox_SetItemData(hWndCtl, n, BELOW_NORMAL_PRIORITY_CLASS);}n = ComboBox_AddString(hWndCtl, TEXT("Idle"));ComboBox_SetItemData(hWndCtl, n, IDLE_PRIORITY_CLASS);ComboBox_SetCurSel(hWndCtl, nNormal);// Initialize thread relative prioritieshWndCtl = GetDlgItem(hWnd, IDC_THREADRELATIVEPRIORITY);n = ComboBox_AddString(hWndCtl, TEXT("Time critical"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_TIME_CRITICAL);n = ComboBox_AddString(hWndCtl, TEXT("Highest"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_HIGHEST);n = ComboBox_AddString(hWndCtl, TEXT("Above normal"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_ABOVE_NORMAL);nNormal = n = ComboBox_AddString(hWndCtl, TEXT("Normal"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_NORMAL);n = ComboBox_AddString(hWndCtl, TEXT("Below normal"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_BELOW_NORMAL);n = ComboBox_AddString(hWndCtl, TEXT("Lowest"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_LOWEST);n = ComboBox_AddString(hWndCtl, TEXT("Idle"));ComboBox_SetItemData(hWndCtl, n, THREAD_PRIORITY_IDLE);ComboBox_SetCurSel(hWndCtl, nNormal);Edit_LimitText(GetDlgItem(hWnd, IDC_SLEEPTIME), 4);// Maximum of 9999Edit_SetText(GetDlgItem(hWnd, IDC_EDITCAPABILITY), TEXT("0")); // init the edit capabilitySetTimer(hWnd, 1, 1000, NULL); // set Timer with interval 1sreturn TRUE;}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {switch (id) {case IDCANCEL:KillTimer(hWnd, 1);PostQuitMessage(0);break;case IDC_PROCESSPRIORITYCLASS:if (codeNotify == CBN_SELCHANGE) {SetPriorityClass(GetCurrentProcess(),(DWORD)ComboBox_GetItemData(hWndCtl, ComboBox_GetCurSel(hWndCtl)));}break;case IDC_THREADRELATIVEPRIORITY:if (codeNotify == CBN_SELCHANGE) {SetThreadPriority(GetCurrentThread(),(DWORD)ComboBox_GetItemData(hWndCtl, ComboBox_GetCurSel(hWndCtl)));}break;case IDC_SUSPEND:// To avoid deadlock, call EnableWindow before creating// the thread that calls SuspendThread.EnableWindow(hWndCtl, FALSE);HANDLE hThreadPrimary;DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),GetCurrentProcess(), &hThreadPrimary,THREAD_SUSPEND_RESUME, FALSE, DUPLICATE_SAME_ACCESS);DWORD dwThreadID;CloseHandle(chBEGINTHREADEX(NULL, 0, ThreadFunc,hThreadPrimary, 0, &dwThreadID));break;}}//////////////////////////////////////////////////////////////////////////void WINAPI Dlg_OnTimer(HWND hwnd, UINT id) {__int64 capabilityCount = currentCount - lastNumCount;__int64 currentTime = stopWath.Now();__int64 capability = capabilityCount * 1000 / (currentTime - lastTimeStart);TCHAR sz[32] = { 0 };StringCchPrintf(sz, _countof(sz), TEXT("%llu"), capability);Edit_SetText(GetDlgItem(hwnd, IDC_EDITCAPABILITY), sz);//lastNumCount = currentCount;lastTimeStart = currentTime;}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);chHANDLE_DLGMSG(hWnd, WM_TIMER, Dlg_OnTimer);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {HWND hWnd =CreateDialog(hInstExe, MAKEINTRESOURCE(IDD_SCHEDLAB), NULL, Dlg_Proc);BOOL fQuit = FALSE;while (!fQuit) {MSG msg;if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {// IsDialogMessage allows keyboard navigation to work properly.if (!IsDialogMessage(hWnd, &msg)) {if (msg.message == WM_QUIT) {fQuit = TRUE;// For WM_QUIT, terminate the loop.}else {// Not a WM_QIT message. Translate it and dispatch it.TranslateMessage(&msg);DispatchMessage(&msg);}} // if(!IsDialogMessage())}else {// Add a number to the listboxstatic __int64 s_n = -1;TCHAR sz[20];currentCount = ++s_n;//record currentcountStringCchPrintf(sz, _countof(sz), TEXT("%llu"), s_n);HWND hWndWork = GetDlgItem(hWnd, IDC_WORK);ListBox_SetCurSel(hWndWork, ListBox_AddString(hWndWork, sz));// Remove some string if there are too many entrieswhile (ListBox_GetCount(hWndWork) > 100)ListBox_DeleteString(hWndWork, 0);// How long should the thread sleepint nSleep = GetDlgItemInt(hWnd, IDC_SLEEPTIME, NULL, FALSE);if (chINRANGE(1, nSleep, 9999))Sleep(nSleep);}}DestroyWindow(hWnd);return 0;}

7.11 关联性

默认情况下系统使用软关联(soft affinity)来给线程分配处理器:

如果其他因素都一样。系统将使线程在上一次运行的处理器上运行。让线程始终在同一个处理器上运行有助于重用仍在处理器高速缓存中的数据。


NUMA(Uon-Uniform Memory Access, 非统一内存访问)计算机体系结构,系统有多快系统板组成,每块系统板有自己的多个cpu和内存。如下图


NUMA在cpu只访问其所在主板上的内存时性能最佳,若访问其他主板上的内存性能下降的很厉害。因此希望cpu将同一进程的所有线程尽量运行在同一块主板的内存和cpu上。

这就需要设置进程和线程的关联性(affinity)。可以设置让哪些CPU运行特定的线程。(硬关联)


调用GetSystemInfo可以查询机器上cpu的数量。

SetProcessAffinityMask 用以限制某些线程只能在可用cpu的一个子集上运行。

BOOLWINAPISetProcessAffinityMask(    _In_ HANDLE hProcess,    _In_ DWORD_PTR dwProcessAffinityMask    );

第一个参数代表要设置的进程,第二个参数dwProcessAffinityMask设置以为掩码,代表线程可以在哪些CPU上运行。

例如0x00000005 代表进程中的线程可以在CPU0 和CPU2上运行,但是不能在CPU1和CPU3-31上运行。

子进程将继承关联性。如果一个进程的关联性掩码为0x00000005 它的所有子进程中的任何线程也将具有相同的掩码。

还可以使用作业对象来限制一组进程只在一组CPU上运行。

JOBOBJECT_BASIC_LIMIT_INFORMATION 的  Affinity成员可以设置此作业上进程的掩码


获取进程的关联性掩码

WINBASEAPIBOOLWINAPIGetProcessAffinityMask(    _In_  HANDLE hProcess,    _Out_ PDWORD_PTR lpProcessAffinityMask,    _Out_ PDWORD_PTR lpSystemAffinityMask    );

第一个参数是进程的句柄。

第二个参数获取进程的关联性掩码

第三个参数返回系统的关联性掩码(如果存在)表示系统中哪个CPU可以运行这些线程。


设置线程的关联性掩码

SetThreadAffinityMask

WINBASEAPIDWORD_PTRWINAPISetThreadAffinityMask(    _In_ HANDLE hThread,    _In_ DWORD_PTR dwThreadAffinityMask    );


该掩码必须是其进程关联性掩码的真子集。 返回值是线程之前的关联性掩码。

例如要限制3个线程只运行在CPU1, CPU2和CPU3上。可以这样

// Thread 0 can only run on CPU 0.SetThreadAffinityMask(hThread0, 0x00000001);// Thread 1, 2, 3 run on CPUs 1, 2, 3.  1110BSetThreadAffinityMask(hThread1, 0x0000000E);SetThreadAffinityMask(hThread2, 0x0000000E);SetThreadAffinityMask(hThread3, 0x0000000E);


x86系统启动是,系统将执行代码,检查主机上哪个PCU存在注明的Pentium FDIV bug。

方法是将一个线程关联性设置为该CPU,执行可能会出错的除法操作,然后比较结构是否与已知正确的结构相符。随后再采取同样步骤检查下一个CPU。

书上给出的例子说明了cpu关联性将影响线程的调度和性能。



有时候强制一个线程只能使用特定的某个cpu并不是什么好主意。可能出现3个线程同时抢占cpu0,而cpu 1,2,3却无所事事。

SetThreadIdealProcessor 给线程设置一个理想的CPU,但若有CPU空闲也可以将它移到另一个CPU上。


WINBASEAPIDWORDWINAPISetThreadIdealProcessor(    _In_ HANDLE hThread,    _In_ DWORD dwIdealProcessor    );


dwIdealProcessor不是掩码,而是0~31/63之间的整数。 可以传入 MAXIMUM_PROCESSOR表示没有线程理想的CPU。函数返回之前理想的CPU。

如果线程没有理想CPU则为MAXIMUM_PROCESSORS


可以在可执行文件的头部设置处理器关联性。(虽然连接器并没有提供类似的功能)利用了ImageHlp.h中声明的一些函数

// Load the EXE into memory.PLOADED_IMAGE pLoadedImage = ImageLoad(szExeName, NULL);// Get the current load configuration information for the EXE.IMAGE_LOAD_CONFIG_DIRECTORY ilcd;GetImageConfigInformation(pLoadedImage, &ilcd);// Change the processor affinity mask.ilcd.ProcessAffinityMask = 0x00000003; // I desire CPUs 0 and 1// Save the new load configuration information.SetImageConfigInformation(pLoadedImage, &ilcd);// Unload the EXE from memoryImageUnload(pLoadedImage);

在任务管理器中也运行设置cpu关联性。


可以在系统启动时限制CPU的数量。系统间检查启动配置数据BCD(boot configuration data)。参考MSDN

https://msdn.microsoft.com/en-us/library/windows/hardware/dn653287(v=vs.85).aspx


BCD可以通过WMI(Windows Management Instrumentaton)实现,例如要限制cpu的数量。

开始-》运行-》Msconfig -》高级可以限制系统启动cpu的数量



补充:使用CP5中的JobLab(作业)来限制本章的Schelab程序在单cpu上执行任务。

设置Affinity Mask 为1  只允许使用CPU0.

可以看到任务管理器使用率仅仅为35%


阅读全文
0 0