线程调度
每一个线程都有一个上下文(CONTEXT结构),它保存在线程内核对象中。这个上下文反映了上一次执行时CPU寄存器的状态。在相同线程优先级的情况下,大约每隔20ms,系统就会扫描所有当前存在的线程内核对象。在这些线程内核对象中,找到一个可调度的线程内核对象,将其上下文的值载入CPU寄存器,以使线程得以执行。这个称为线程上下文切换。
整体流程是:当一个线程执行到一个CPU时间片结束时,线程会保存当前寄存器的状态,以确保下一个时间片到来时,线程能得以继续执行。
Windows是一个抢占式操作系统,系统可以再任何时刻停止一个线程而去调度另外的线程。所以我们无法保证线程总在运行,线程会得到整个处理器,系统不允许运行其他线程等。
线程挂起和恢复:可以再创建线程时指定CREATE_SUSPENDED标志来挂起线程,当我们创建线程时,初始线程的内核对象中的挂起计数为1,然后创建函数会查看线程是否设置了CREATE_SUSPENDED标志,如果没指定,函数会递减内核对象的挂起计数;也可以是用SuspendThread函数来挂起线程,使用ResumeThread函数来恢复线程。但是SuspendThread和ResumeThread是异步的,所以在挂起和恢复的时候必须小心。
线程的睡眠和切换:调用Sleep函数,可以使线程放弃剩下的CPU时间片,线程睡眠的时间并不精确,因为我们不能保证睡眠时间到后,系统能马上分配时间片给应用程序。系统提供了一个SwitchToThread函数,调用这个函数时,系统会检测是否存在正急需CPU时间的饥饿线程。如果没有,函数立即返回。如果存在函数将调度该线程。
获取线程执行时间:GetTickCount64函数返回从系统启动到现在的时间。有些人常使用这个函数来获取线程中某一段代码的执行时间,这是不精确。因为我们不知道在这段时间内,CPU时间片是否已经结束而等待另一时间片。我们可以是使用
BOOL GetThreadTimes (
HANDLE hThread,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime);
函数返回四个值(单位为100ns),pftCreationTime(创建时间)、pftExitTime(退出时间)都是表示从格林尼治时间1601年1月1日子夜开始计算的时间,pftKernelTime(内核时间)表示线程执行内核模式下的操作系统代码所用时间的绝对值,pftUserTime(用户时间)表示线程执行应用程序代码所用时间的绝对值。计算代码时间 = 内核时间之差 + 用户时间之差。
线程优先级
上面说过,在调度程序给另一个可调度程序分配CPU之前,CPU可以运行一个线程大约20ms。这是线程优先级相同的情况,而在系统中的线程有不同的优先级,这将影响调度程序如何选择下一个可调度线程。每一个线程都被赋予一个0~31的优先级数,但是我们并不能直接操作和设置这个优先级数,我们需要通过设置进程的优先级类和线程的相对优先级来间接设置线程的优先级值。系统分配CPU时,会先分配给优先级高的线程,那这样岂不是会照成优先级低的线程得不到执行吗?错了,因为线程不总是可调度的,比如当线程消息队列中没有消息时,系统将停止这个线程。
相对线程优先级 |
进程优先级类 |
|||||
Idle |
Below normal |
Normal |
Above norma |
High |
Real-time |
|
Time-critical |
15 |
15 |
15 |
15 |
15 |
31 |
Highest |
6 |
8 |
10 |
12 |
15 |
26 |
Above nomal |
5 |
7 |
9 |
11 |
14 |
25 |
Normal |
4 |
6 |
8 |
10 |
13 |
24 |
Below normal |
5 |
5 |
7 |
9 |
12 |
23 |
Lowest |
2 |
4 |
6 |
8 |
11 |
22 |
idle |
1 |
1 |
1 |
1 |
1 |
16 |
进程优先级和相对线程优先级与优先级值的映射
SetPriorityClass和GetPriorityClass函数可以设置和获取进程优先级类的值,SetThreadPriority和GetThreadPriority函数可以设置和获取线程相对优先级的值。
系统有一个动态提升线程优先级的功能,意思是当线程未获得消息处于不可调度时,假如有键盘消息载入线程的消息队列,线程就变成可调度的了,键盘设备驱动程序将使系统临时提升线程的优先级。系统只提升优先级值在1~15的线程,这个范围称为动态优先级范围。系统不会把线程的优先级提升到实时范围(高于15)。系统不会动态提升实时范围(16~31)的线程。我们可以允许和进程线程优先级进行动态提升。
BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL bDisablePriorityBoost);
BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL bDisablePriorityBoost);
BOOL SetThreadPriorityBoost (HANDLE hThread, BOOL bDisablePriorityBoost);
BOOL GetThraedPriorityBoost(HANDLE hThread, BOOL bDisablePriorityBoost);
SetProcessPriorityBoost和GetProcessPriorityBoost 设置或获取系统提升一个进程中所有线程的优先级;
SetThreadPriorityBoost和GetThraedPriorityBoost 设置或获取提升某个线程的优先级。
I/O请求优先级
如果一个低优先级的线程获得CPU时间片,它可以在很短时间内将成千个I/O请求载入队列,由于I/O请求一般都需要一定时间来处理,所以可能低优先级的线程挂起高优先级的线程,使其不能完成任务。要解决这个问题,我们可以在进行I/O请求时设置优先级。通过SetThreadPriority传入THREAD_MODE_BACKGROUND_BEGIN来告诉windows,线程应该发送低优先级的I/O请求。传入THREAD_MODE_BACKGROUND_END时,让线程发出normal优先级的I/O请求。SetFileInformationByHandle函数设置的优先级将覆盖进程的优先级或者线程,即分别通过SetPriorityClass或者SetThreadPriority函数设置的优先级。
线程关联性
WindowsNT在给线程分配处理器时,使用软关联。即如果其他因素都一样,系统将使线程在上一次运行的处理器上运行。让线程始终在同一个处理器上运行有助于重用仍在处理器高速缓存中的数据。
BOOL SetProcessAffinityMask(Handle hProcess, DWORD_PTR dwProcessAffinityMask);
BOOL GetProcessAffinityMask(Handle hProcess, PDWORD_PTR dwProcessAffinityMask, PWORD_PTR pdwSystemAffinityMask);
SetProcessAffinityMask函数可以限定进程的所有线程的关联性掩码(即特定运行CPU)。
GetProcessAffinityMask函数可以获取进程的关联系掩码。
DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinity);
函数可以分别设置各线程的关联性掩码(即在特定的CPU上运行)。
如果我们想给线程设置一个理想的CPU(即允许将线程移动到空闲的CPU上),我们可以调用
DWORD SetThreadIdealProcessor(HANDLE hThread, DWORD dwIdealProcessor);
dwIdealProcessor是一个0到31/63的整数,表示线程希望设置的CPU。