第5章 不要让线程成为脱缰的野马(Keeping your Threads on Leash) ---线程优先权(Thread priority)
有没有过这样的经验?你坐在你的车子里,目的地还在好几公里之遥,而时间已经很晚了。你拼命想告诉那些挡住你去路的人们,今天这个约会对你是多么多么重要,能不能请他们统统……呃……滚到马路外?很不幸,道路系统并没有纳入所谓的优先权观念。如果有某条专用道是给“非常重要”的通行所用的,你就可以摆脱那些如潮水般在你四周的车辆和行人,岂不甚妙?
Win32 有所谓的优先权(priority)观念,用以决定下一个获得 CPU 时间的线程是谁。较高优先权的线程必然获得较多的 CPU 时间。关于优先权的完整讨论其实相当复杂。你可以无分轩轾地给予每一个线程相同的优先权,这可能会使你承担不少麻烦。你也可以明智地使用优先权,使自己能够调整程序的执行次序。例如你可以设定你的 GUI 线程有较高优先权,使它对于用户的反应能够比较平顺一些,或者你可以改变 worker 线程的优先权,使它们只在系统的闲置时间(idle time)里工作。
Win32 优先权是以数值表现的,并以进程的“优先权类别(priority class)”、线程的“优先权层级 (priority level)”和操作系统当时采用的“动态提升(Dynamic Boost)”作为计算基准。所有因素放在一起,最后获得一个 0~31 的数值。拥有最高优先权之线程,即为下一个将执行起来的线程。如果你有一大把 worker 线程,其“优先权类别”和“优先权层级”都相同,那么就每一个轮流执行。这是所谓的 “round robin” 调度方式。如果你有一个线程总是拥有最高优先权,那么它就永远获得 CPU 时间,别人都别玩了。这就是为什么必须明智而谨慎地使用优先权的原因。
优先权类别(Priority Class)
“优先权类别”是进程的属性之一。这个属性可以表现出这一进程和其他进程比较之下的重要性。Win32 提供四种优先权类别,每一个类别对应一个基本的优先权层级。表格5-1 展示了四个优先权类别。
表格5-1 优先权类别(Priority Classes)
优先权类别(Priority Classes) 基础优先权值(base priority)
HIGH_PRIORITY_CLASS 13
IDLE_PRIORITY_CLASS 4
NORMAL_PRIORITY_CLASS 7 or 8(译注:有些资料上写 7 or 9)
REALTIME_PRIORITY_CLASS 24
大部分程序使用 NORMAL_PRIORITY_CLASS。少数情况下才会考虑使用其他类别。例如,Task Manager 就是使用 HIGH_PRIORITY_CLASS,所以即使其他程序处于非常忙碌的状态下,它也总是能够有所反应。
Windows NT 中有一个好例子,可以说明到底应不应该使用某些特定的优先权类别。以 OpenGL 完成的屏幕保护程序(screen saver)看似密集地使用了所有 CPU 时间。如果这个屏幕保护程序启动时你正在进行系统备份,备份操作会慢下来, 像蜗牛一样。但如果屏幕保护程序使用IDLE_PRIORITY_CLASS,它就只会在 CPU 绝对空闲的时候才执行。
最后一个类别是 REALTIME_PRIORITY_CLASS。这个类别用以协助解决一些和时间有密切关系的工作。举个例子,如果有个程序必须反应一个设备驱动程序的行为,而该驱动程序用来实时监控(real-time monitoring)真实世界中的一台仪器,那么将该进程设为这个优先权类别,就可以使它甚至优于核心进程和设备驱动程序。这个优先权类别不应该用于标准 GUI 程序或甚至于典型的服务器程序。
优先权类别适用于进程而非线程。你可以利用 SetPriorityClass() 和GetPriorityClass() 来调整和验证其值。本书并未涵盖这两个函数的说明。
优先权层级(Priority Level)
线程的优先权层级(Priority Level)是对进程的优先权类别的一个修改,使你能够调整同一个进程内的各线程的相对重要性。一共有七种优先权层级,显示于表格5-2 中。
表格5-2 优先权层级(Priority Levels)
优先权层级(Priority Levels) 调整值
THREAD_PRIORITY_HIGHEST +2
THREAD_PRIORITY_ABOVE_NORMAL +1
THREAD_PRIORITY_NORMAL 0
THREAD_PRIORITY_BELOW_NORMAL –1
THREAD_PRIORITY_LOWEST –2
THREAD_PRIORITY_IDLE Set to 1
THREAD_PRIORITY_TIME_CRITICAL Set to 15
注意:对于 REALTIME_PRIORITY_CLASS 的调整值,有点不同于上表所列。
优先权层级可以利用 SetThreadPriority() 改变之。
BOOL SetThreadPriority(
HANDLE hThread,
int nPriority
);
参数
hThread 代表欲调整优先权的那个线程。
nPriority 表格5-2 所显示的数值。
返回值
如果函数成功,就传回表格5-2 所列的其中一个值。如果函数失败,就传回 FALSE。GetLastError() 可以获得更详细的信息。
线程目前的优先权层级可以利用 GetThreadPriority() 获知。
int GetThreadPriority(
HANDLE hThread
);
参数
hThread 代表一个线程
返回值
如果函数成功, 就传回 TRUE 。如果函数失败, 就传回THREAD_PRIORITY_ERROR_RETURN。GetLastError() 可以获得更详细的信息。
KERNEL32.DLL 中的优先权
我使用 Windows 95 所提供的 PVIEW32,观察我的系统中的各个进程,结果如图5-1 所示。我看到系统模块 KERNEL32.DLL 有八个线程,其优先权类别是 HIGH_PRIORITY_CLASS,所以其基础优先权值为 13。检查其线程,发现有四个线程的优先权层级是 THREAD_PRIORITY_LOWEST,所以其优先权为11。三个线程的优先权层级是 THREAD_PRIORITY_NORMAL,所以其优先权为 13。一个线程的优先权层级是 THREAD_PRIORITY_TIME_CRITICAL,所以其优先权为 15。最后这个线程应该总是能够在任何其他“非实时线程”之前被调度程序选中执行。
图5-1 Windows 95 中的PVIEW32(图因win版本过旧省略)
动态提升(Dynamic Boost)
决定线程真正优先权的最后一个因素是其目前的动态提升值(Dynamic Boost)。所谓动态提升是对优先权的一种调整,使系统能够机动对待线程,以强化程序的可用性。
最容易被我们观察的,便是 Windows NT 施行于所有前台程序的“线程动态提升”。图5-2 的 系统属性 中的【性能】附页,允许用户指定前台程序应该对用户有怎样的回应。你可以在【我的电脑】中按下右键,并选择【属性】而获得这一画面。
默认情况下图5-2 的“动态提升”被设定为最大,这使得拥有键盘焦点的程序(前台程序)的优先权得以提升 +2。这个设定使得前台程序比后台程序获得较多的 CPU 时间,因此即使系统忙碌,前台程序还是容易保持其 UI 敏感度。
图5-2 Windows NT 4.0 的系统属性(图因win版本过旧省略)
第二种优先权动态提升也适用于同属一个进程的线程,用以反应用户的输入或磁盘的输入。例如,只要线程获得键盘输入,该线程就得到一个 +5 的优先权调整值。这使得该线程有机会处理那个输入,并且提供立即的回应给用户。其他可能引起优先权动态提升的情况还包括鼠标消息、计时器消息等等。
最后一种优先权动态提升的情况可能发生在任何一个线程(不限属于哪一个进程)身上。那是在一个“等待状态”获得满足时发生的,例如有一个线程正在等待一个 mutex,当 Wait...() 返回时,该线程的优先权会获得动态提升。这样的提升意味着 critical sections 将尽可能地被快速处理,而等待时间将尽可能地缩短。
更令人战栗的 Busy Waiting
你已经在第2章看到了,一个 busy loop 是如何地吃掉 CPU 时间。一旦你开始调整线程优先权,情况有可能变得更糟。书附盘片中有一个程序名为BUSYPRIO , 以 THREAD_PRIORITY_HIGHEST 来运行主线程, 以THREAD_PRIORITY_NORMAL 来运行 worker 线程。
如果你执行这个程序,你可能会看到一些非所期望的结果:程序永远结束不了。为什么?主线程不断等待,所以不断需要 CPU 时间。而由于它的优先权比 worker 线程高,所以 worker 线程永远没有机会获得 CPU 时间。这种情况称为 starvation(饥饿)。
BUSYPRIO 显示,小心翼翼地设定线程优先权是件多么重要的事情。改变线程优先权可能会打开潘朵拉的盒子,一些新的问题跑出来,死锁的阴影也潜在性地酝酿着。虽然优先权的基础知识很简单,但其实用面却可能很复杂。如果你的目标是保持简单,那就还是避免处理“优先权”这个烫手山芋吧。