如何在某个线程内终止另一个正在运行的线程?
1.利用TerminateThread() 放弃一个线程
BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );
返回值
如果函数成功,则传回 TRUE。如果失败,则传回 FALSE。GetLastError()可以获知更多细节。
TerminateThread() 看起来不错,但是“TerminateThread() 是一个危险的函数,应该在最不得已的情况下才使用”。
TerminateThread() 强迫其行动目标(一个线程)结束,手段激烈而有力,甚至不允许该线程有任何“挣扎”的机会。这带来的副作用便是,线程没有机会在结束前清理自己。对线程而言,这可能导致前功尽弃。这个函数不会在目标线程中丢出一个异常情况(exception),目标线程在核心层面就被根本抹杀了。目标线程没有机会捕捉所谓的“结束请求”,并从而获得清理自己的机会。
还有另一个令人不愉快的情况。目标线程的堆栈没有被释放掉,于是可能会引起一大块内存泄(memory leak)。而且,任何一个与此线程有附着关系的 DLLs 也都没有机会获得“线程解除附着”的通知。
此函数唯一可以预期并依恃的是,线程 handle 将变成激发状态(译注:因为线程结束了),并且传回 dwExitCode 所指定的结束代码。
这个函数所带来的隐伏危机还包括:如果线程正进入一个 critical section之中,该 critical section 将因此永远处于锁定状态,因为 critical section 不像mutex 那样有所谓的 "abandoned" 状态。如果目标线程正在更新一份数据结构,这份数据结构也将永远处于不稳定状态。没有任何方法可以阻止这些问题的发生。所以离TerminateThread() 远远地!
2.使用信号(Signals)
下一个似乎可行的想法是使用 signals。在 Unix 系统中,signals 是跨进程传送通告(notifications)的标准方法。在 Unix 系统中 SIGTERM 相当于“请你离开”的意思,SIGKILL 则是粗略相当于 TerminateThread()。
这个点子似乎不错,因为 C runtime library 支持标准的 signals,如SIGABRT 和 SIGINT。各种 signals 的处理函数可以利用 C 函数 signal() 设立之。
但是我很快就进入了一个死胡同。C runtime 函数中没有一个名为 kill(),而那是 Unix 系统藉以送出 signal 的操作。是有一个 raise() 啦,但只能够传送 signal 给目前的线程。
观察过 C runtime library 的源代码之后,我发现 signals 其实是利用Win32 的异常情况(exceptions)模拟的,Win32 之中并没有真正的 signals,所以这个想法也行不通。
跨越线程,丢出异常情况(Exceptions)
我真正要做的就是在目标线程中引发一个异常情况(exception)。如果有必要在结束前清理某些东西,目标线程可以设法捕捉此一异常情况,否则它可以什么都不管地直接结束自己的生命。
Win32 API 中没有什么标准方法可以把一个异常情况丢到另一个线程中。
3.设立一个标记
当所有方法都失败时,不妨返朴归真,回到最简单最明白的路上。Win32 核准的做法是在你的程序代码中设立一个标记,利用其值来要求线程结束自己。
这个技术有十分明显的优点,可以保证目标线程在结束之前有安全而一致的状态。其缺点也十分明显:线程需要一个 polling 机制,时时检查标记值,以决定该不该结束自己。此刻的你听到 “polling” 会不会有毛骨悚然的感觉?不不,我们并不是要写一个 busy loop 来检验标记值,我们的做法是使用一个手动重置(manual-reset)的 event 对象。Worker 线程可以检查该 event 对象的状态或是等待它,视情况而定。
结束一个线程,听起来好容易,但是结束程序必须按次序进行,以避免发生 race conditions。让程序依次序进行是非常重要的,特别是在程序要结束之前。
4.线程优先权(Thread Priority)
Win32 有所谓的优先权(priority)观念,用以决定下一个获得 CPU 时间的线程是谁。较高优先权的线程必然获得较多的 CPU 时间。关于优先权的完整讨论其实相当复杂。你可以无分轩轾地给予每一个线程相同的优先权,这可能会使你承担不少麻烦。你也可以明智地使用优先权,使自己能够调整程序的执行次序。例如你可以设定你的 GUI 线程有较高优先权,使它对于用户的反应能够比较平顺一些,或者你可以改变 worker 线程的优先权,使它们只在系统的闲置时间(idle time)里工作。
Win32 优先权是以数值表现的,并以进程的“优先权类别(priorityclass)”、线程的“优先权层级 (priority level)”和操作系统当时采用的“动态提升(Dynamic Boost)”作为计算基准。所有因素放在一起,最后获得一个 0~31 的数值。拥有最高优先权之线程,即为下一个将执行起来的线程。如果你有一大把 worker 线程,其“优先权类别”和“优先权层级”都相同,那么就每一个轮流执行。这是所谓的 “round robin” 调度方式。如果你有一个线程总是拥有最高优先权,那么它就永远获得 CPU 时间,别人都别玩了。这就是为什么必须明智而谨慎地使用优先权的原因。
4.1 优先权类别(Priority Class)
“优先权类别”是进程的属性之一。这个属性可以表现出这一进程和其他进程比较之下的重要性。Win32 提供四种优先权类别,每一个类别对应一个基本的优先权层级。下图展示了四个优先权类别。
大部分程序使用 NORMAL_PRIORITY_CLASS。少数情况下才会考虑使用其他类别。例如,Task Manager 就是使用 HIGH_PRIORITY_CLASS,所以即使其他程序处于非常忙碌的状态下,它也总是能够有所反应。
最后一个类别是 REALTIME_PRIORITY_CLASS。这个类别用以协助解决一些和时间有密切关系的工作。举个例子,如果有个程序必须反应一个设备驱动程序的行为,而该驱动程序用来实时监控(real-time monitoring)真实世界中的一台仪器,那么将该进程设为这个优先权类别,就可以使它甚至优于核心进程和设备驱动程序。这个优先权类别不应该用于标准 GUI 程序或甚至于典型的服务器程序。
优先权类别适用于进程而非线程。你可以利用 SetPriorityClass() 和GetPriorityClass() 来调整和验证其值。
4.2 优先权层级(Priority Level)
线程的优先权层级(Priority Level)是对进程的优先权类别的一个修改,使你能够调整同一个进程内的各线程的相对重要性。一共有七种优先权层级。
优先权层级可以利用 SetThreadPriority() 改变之。
BOOL SetThreadPriority( HANDLE hThread, int nPriority );
返回值
如果函数成功,就传回表格5-2 所列的其中一个值。如果函数失败,就传回 FALSE。GetLastError() 可以获得更详细的信息。
线程目前的优先权层级可以利用 GetThreadPriority() 获知。
int GetThreadPriority( HANDLE hThread );参数
4.3 动态提升(Dynamic Boost)
决定线程真正优先权的最后一个因素是其目前的动态提升值(DynamicBoost)。所谓动态提升是对优先权的一种调整,使系统能够机动对待线程,以强化程序的可用性。
最容易被我们观察的,便是 Windows NT 施行于所有前台程序的“线程动态提升”。 系统属性 中的【性能】附页,允许用户指定前台程序应该对用户有怎样的回应。你可以在【我的电脑】中按下右键,并选择【属性】而获得这一画面。
第二种优先权动态提升也适用于同属一个进程的线程,用以反应用户的输入或磁盘的输入。例如,只要线程获得键盘输入,该线程就得到一个 +5 的优先权调整值。这使得该线程有机会处理那个输入,并且提供立即的回应给用户。其他可能引起优先权动态提升的情况还包括鼠标消息、计时器消息等等。
最后一种优先权动态提升的情况可能发生在任何一个线程(不限属于哪一个进程)身上。那是在一个“等待状态”获得满足时发生的,例如有一个线程正在等待一个 mutex,当 Wait...() 返回时,该线程的优先权会获得动态提升。这样的提升意味着 critical sections 将尽可能地被快速处理,而等待时间将尽可能地缩短。
5.初始化一个线程
使用线程的一个常见问题就是如何能够在一个线程开始运行之前,适当地将它初始化。初始化最常见的理由就是为了调整优先权。另一个理由是为了在SMP 系统中设定线程比较喜欢的 CPU。
基本问题在于,你需要一个线程 handle,才能够调整线程的性质。但如果你以默认型式调用CreateThread(),新线程会如脱缰野马一下子就起跑了,你根本来不及进行初始化设定操作。
解决之道就是 CreateThread() 的第5个参数,它允许你指定线程诞生时的属性。目前只定义有一种属性,就是 CREATE_SUSPENDED。这个属性告诉CreateThread() 说:产生一个新线程,传回其 handle,但不要马上开始执行之。
一旦线程设定妥当,你可以调用 ResumeThread() 开始执行:
DWORD ResumeThread( HANDLE hThread );参数
返回值
如果函数成功,则传回线程的前一个挂起次数。如果失败,则传回0xFFFFFFFF。GetLastError() 可以获得更详细的信息。
挂起(suspending)一个线程
相对于 ResumeThread(),毫不令人惊讶地,有一个 SuspendThread()函数。这个函数允许调用端指定一个线程睡眠(挂起)。直到又有人调用了 ResumeThread(),线程才会醒来。因此,睡眠中的线程不可能唤醒自己。这个函数的规格如下:
DWORD SuspendThread( HANDLE hThread );参数
返回值
如果函数成功,则传回线程目前的挂起次数。如果失败,则传回0xFFFFFFFF。GetLastError() 可以获得更详细的信息。
SuspendThread() 是另一个可能会潜在引发问题的函数。考虑一下这种情况:一个进程拥有三个线程A,B,C。线程C正在某个 critical section 内,而线程B正在等它出来。然后,线程A挂起了线程C。在这种情况下,线程C将永远不会离开 critical section,而线程B也就相当于进入了死锁状态。
SuspendThread() 的最大用途就是用来协助撰写调试器。调试器允许在程序员的控制之下,启动或停止任何一个线程。