第6章 线程的基础知识
理解线程是非常关键的,因为每个进程至少需要一个线程。本章将更加详细地介绍线程的知识。尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用。还要介绍系统如何使用线程内核对象来管理线程。与进程内核对象一样,线程内核对象也拥有属性,我们将要观察许多用于查询和修改这些属性的函数。此外还要介绍可以在进程中创建和生成更多的线程时所用的函数。
第4章介绍了进程是由两个部分构成的,一个是进程内核对象,另一个是地址空间。同样,线程也是由两个部分组成的:
• 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
• 另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量(第1 6章将进一步介绍系统如何管理线程堆栈)。
第4章中讲过,进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
如你所见,进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间需要许多系统资源。系统中要保留大量的记录,这要占用大量的内存。另外,由于. e x e和. d l l文件要加载到一个地址空间,因此也需要文件资源。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。
由于线程需要的开销比进程少,因此始终都应该设法用增加线程来解决编程问题,而要避免创建新的进程。但是,这个建议并不是一成不变的。许多程序设计用多个进程来实现会更好些。应该懂得权衡利弊,经验会指导你的编程实践。
在详细介绍线程之前,首先花一点时间讲一讲如何正确地在应用程序结构中使用线程。
6.1 何时创建线程
线程用于描述进程中的运行路径。每当进程被初始化时,系统就要创建一个主线程。该线程与C / C + +运行期库的启动代码一道开始运行,启动代码则调用进入点函数( m a i n、w m a i n、Wi n M a i n或w Wi n M a i n),并且继续运行直到进入点函数返回并且C / C + +运行期库的启动代码调用E x i t P r o c e s s为止。对于许多应用程序来说,这个主线程是应用程序需要的唯一线程。不过,进程能够创建更多的线程来帮助执行它们的操作。
每个计算机都拥有一个功能非常强大的资源,即C P U。让C P U闲置起来是绝对没有道理的(如果忽略节省电能问题的话)。为了使C P U处于繁忙状态之中,可以让它执行各种不同的工作。下面是一些例子:
• 可以打开Microsoft Windows 2000配备的内容索引服务程序。它能够创建一个低优先级的线程,以便定期打开你的磁盘驱动器上的文件内容并给内容做索引。若要找到一个文件,可以打开Search Result(搜索结果)窗口(方法是单击S t a r t按钮,从S e a r c h菜单中选定For Files Or Folders),再将你的搜索条件输入Containing Te x t域。这时就可以搜索到索引,相关的文件就会立即显示出来。内容索引服务程序大大改进了性能,因为每次搜索不必打开、扫描和关闭磁盘驱动器上的每个文件。
• 可以使用Windows 2000配备的磁盘碎片整理软件。通常情况下,这种类型的实用程序拥有许多管理选项,一般用户可能不懂,比如该实用程序应该相隔多长时间运行一次,何时运行。使用低优先级线程,可以在后台运行该实用程序,并且在系统空闲时对驱动器进行碎片整理。
• 可以很容易地设想将来版本的编译器,每当暂停键入时,它就可以自动编译你的源代码文件。输出窗口可以向你(几乎)实时显示警告和出错信息。当键入变量和函数名时出现错误时,就能立即发现。在某种程度上讲, Microsoft Visual Studio已经实现了这个功能,使用Wo r k s p a c e的C l a s s Vi e w窗格,就能够看到这些信息。
• 电子表格应用程序能够在后台执行各种计算。
• 字处理程序能够执行重新分页、拼写和语法检查及在后台进行打印。
• 文件可以在后台拷贝到其他介质中。
• We b浏览器在后台与它们的服务器进行通信。因此,在来自当前We b站点的结果输入之前,用户可以缩放浏览器的窗口或者转到另一个Web站点。
这些例子中,有一个重要问题应该注意,那就是多线程能够简化应用程序的用户界面。如果每当停止键入时,编译器建立了你的应用程序,那么就没有必要提供B u i l d菜单选项。文字处理应用程序不需要Check Spelling(拼写检查)和Check Grammar(语法检查)菜单选项。
在We b浏览器的例子中,注意,将不同的线程用于I / O(网络、文件或其他),应用程序的用户界面就能够始终保持工作状态。比如有一个应用程序负责给数据库记录进行排序、打印文档或拷贝文件。如果将独立的线程用于处理这个与I / O相关的任务,用户就可以在进程中继续使用应用程序界面来取消操作。
设计一个拥有多线程的应用程序,就会扩大该应用程序的功能。我们在下一章中可以看到,每个线程被分配了一个C P U。因此,如果你的计算机拥有两个C P U,你的应用程序中有两个线程,那么两个C P U都将处于繁忙状态。实际上,你是让两个任务在执行一个任务的时间内完成操作。
每个进程至少拥有一个线程。因此,如果你在应用程序中不执行任何特殊的操作,在多进程操作系统上运行,就能够得到许多好处。例如,可以建立一个应用程序,并同时使用文字处理程序(我常常这样做)。如果计算机拥有两个C P U,那么该应用程序就可以在一个处理器上执行,而另一个处理器则负责处理文档。另外,如果编译器出现一个错误,导致它的线程进入一个无限循环,仍然可以使用其他的进程( 1 6位Wi n d o w s和M S - D O S应用程序则不行)
6.2 何时不能创建线程
至今为止,一直在讨论多线程应用程序的优点。虽然多线程应用程序的优点很多,但是它也存在某些不足之处。有些开发人员认为,解决问题的方法是将它分割成多个线程。这种想法是完全错误的。
线程确实是非常有用的,但是,当使用线程时,在解决原有的问题时可能产生新的问题。例如,你开发了一个文字处理应用程序,并且想要让打印函数作为它自己的线程来运行。这听起来是个很好的主意,因为用户可以在打印文档时立即回头着手编辑文档。但是,这意味着文档中的数据可能在文档打印时变更。也许最好是不要让打印操作在它自己的线程中发生,不过这种“方案”看起来有点儿极端。如果你让用户编辑另一个文档,但是锁定正在打印的文档,使得打印结束前该文档不能修改,那将会怎样呢?这里还有第三种思路,将文档拷贝到一个临时文件,然后打印该临时文件的内容,并让用户修改原始文档。当包含该文档的临时文件结束打印时,删除临时文件。
如你所见,线程能够解决某些问题,但是却又会产生新的问题。在开发应用程序的用户界面时,很可能出现对线程的另一种误用。几乎在所有的应用程序中,所有用户界面的组件(窗口)应该共享同一个线程。单个线程应该创建窗口的所有子窗口。有时在不同的线程上创建不同的窗口是有用的,不过这种情况确实非常少见。
通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个G e t M e s s a g e循环。进程中的所有其他线程都是工作线程,它们与计算机或I / O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。
虽然单个进程拥有多个用户界面线程的情况并不多见,但是这种情况有着某种有效的用途。Windows Explorer为每个文件夹窗口创建了一个独立的线程。它使你能够将文件从一个文件夹拷贝到另一个文件夹,并且仍然可以查看你的系统上的其他文件夹。另外,如果E x p l o r e r中存在一个错误,那么负责处理文件夹的线程可能崩溃,但是仍然能够对其他文件夹进行操作,至少在执行的操作导致其他文件夹也崩溃之前,仍然可以对它们进行操作(关于线程和用户界面的详细说明,参见第2 6和2 7章)。
上述内容的实质是应该慎重地使用多线程。不要想用就用。仅仅使用赋予进程的主线程,就能够编写出许多非常有用的和功能强大的应用程序。
6.3 编写第一个线程函数
每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。前面已经介绍了主线程的进入点函数:即m a i n、w m a i n、Wi n M a i n或w Wi n M a i n。如果想要在你的进程中创建一个辅助线程,它必定也是个进入点函数,类似下面的样子:
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
DWORD dwResult = 0;
...
return(dwResult);
}
你的线程函数可以执行你想要它做的任何任务。最终,线程函数到达它的结尾处并且返回。这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果使用计数降为0,线程的内核对象就被撤消。与进程内核对象的情况相同,线程内核对象的寿命至少可以达到它们相关联的线程那样长,不过,该对象的寿命可以远远超过线程本身的寿命。
下面对线程函数的几个问题作一说明:
• 主线程的进入点函数的名字必须是m a i n、w m a i n、Wi n M a i n或w Wi n M a i n,与这些函数不同的是,线程函数可以使用任何名字。实际上,如果在应用程序中拥有多个线程函数,必须为它们赋予不同的名字,否则编译器/链接程序会认为你为单个函数创建了多个实现函数。
• 由于给你的主线程的进入点函数传递了字符串参数,因此可以使用A N S I / U n i c o d e版本的进入点函数: m a i n / w m a i n和Wi n M a i n / w Wi n M a i n。可以给线程函数传递单个参数,参数的含义由你而不是由操作系统来定义。因此,不必担心A N S I / U n i c o d e问题。
• 线程函数必须返回一个值,它将成为该线程的退出代码。这与C / C + +运行期库关于让主线程的退出代码作为进程的退出代码的原则是相似的。
• 线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,这可能破坏变量的内容。然而,参数和局部变量是在线程堆栈中创建的,因此它们不太可能被另一个线程破坏。
既然懂得了实现线程函数的方法,下面讲述如何让操作系统来创建能够执行线程函数的线程。
6.4 CreateThread函数
前面已经讲述了调用C r e a t e P r o c e s s函数时如何创建进程的主线程。如果想要创建一个或多个辅助函数,只需要让一个已经在运行的线程来调用C r e a t e T h r e a d:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadID);
当C r e a t e T h r e a d被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。这与进程和进程内核对象之间的关系是相同的。
系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
注意C r e a t e T h r e a d函数是用来创建线程的Wi n d o w s函数。不过,如果你正在编写C / C + +代码,决不应该调用C r e a t e T h r e a d。相反,应该使用Visual C++运行期库函数_ b e g i n t h r e a d e x。如果不使用M i c r o s o f t的Visual C++编译器,你的编译器供应商有它自己的C r e a t e T h r e d替代函数。不管这个替代函数是什么,你都必须使用。本章后面将要介绍_ b e g i n t h r e a d e x能够做什么,它的重要性何在。
这就是Create Thread函数的概述,下面各节将要具体介绍C r e a t e T h r e a d的每个参数。
6.4.1 psa
p s a参数是指向S E C U R I T Y _ AT T R I B U T E S结构的指针。如果想要该线程内核对象的默认安全属性,可以(并且通常能够)传递N U L L。如果希望所有的子进程能够继承该线程对象的句柄,必须设定一个S E C U R I T Y _ AT T R I B U T E S结构,它的b I n h e r i t H a n d l e成员被初始化为T R U E。详细信息参见第3章。
6.4.2 cbStack
c b S t a c k参数用于设定线程可以将多少地址空间用于它自己的堆栈。每个线程拥有它自己的堆栈。当C r e a t e P r o c e s s启动一个进程时,它就在内部调用C r e a t e T h r e a d来对进程的主线程进行初始化。对于c b S t a c k参数来说,C r e a t e P r o c e s s使用存放在可执行文件中的一个值。可以使用链接程序的/ S TA C K开关来控制这个值:
/STACK:[reserve] [,commit]
r e s e r v e参数用于设定系统应该为线程堆栈保留的地址空间量。默认值是1 MB。C o m m i t参数用于设定开始时应该承诺用于堆栈保留区的物理存储器的容量。默认值是1页。当线程中的代码执行时,可能需要多个页面的存储器。当线程溢出它的堆栈时,就生成一个异常条件(关于线程堆栈和堆栈溢出的异常条件的详细说明,参见第1 6章,关于一般异常条件的处理的详细说明,参见第2 3章)。系统抓取该异常条件,并且将另一页(或者你为c o m m i t参数设定的任何值)用于保留空间,这使得线程的堆栈能够根据需要动态地扩大。
当调用C r e a t e T h r e a d时,如果传递的值不是0,就能使该函数将所有的存储器保留并分配给线程的堆栈。由于所有的存储器预先作了分配,因此可以确保线程拥有指定容量的可用堆栈存储器。保留空间的容量既可以是/ S TA C K链接程序设定的容量,也可以是C b S t a c k的值,谁大就用谁。分配的存储器容量应该与传递的c b S t a c k值相一致。如果将0传递给C b S t a c k参数,C r e a t e T h r e a d就保留一个区域,并且将链接程序嵌入. e x e文件的/ S TA C K链接程序开关信息指明的存储器容量分配给线程堆栈。
保留空间的容量用于为堆栈设置一个上限,这样就可以抓住代码中的循环递归错误。例如,你编写一个递归自调用函数,该函数也包含导致循环递归的一个错误。每次函数调用自己的时候,堆栈上就创建一个新的堆栈框。如果系统不设定堆栈的最大值,该递归函数就永远不会停止对自己的调用。进程的所有地址空间将被分配,大量的物理存储器将被分配给该堆栈。通过设置一个堆栈限制值,就可以防止应用程序用完大量的物理存储器,同时,也可以更快地知道何时程序中出现了错误(第1 6章中的S u m m a t i o n示例应用程序显示了如何跟踪和处理应用程序中的堆栈溢出)。
6.4.3 pfnStartAddr和pvParam
p f n S t a r t A d d r参数用于指明想要新线程执行的线程函数的地址。线程函数的p v P a r a m参数与原先传递给C r e a t e T h r e a d的p v P a r a m参数是相同的。C r e a t e T h r e a d使用该参数不做别的事情,只是在线程启动执行时将该参数传递给线程函数。该参数提供了一个将初始化值传递给线程函数的手段。该初始化数据既可以是数字值,也可以是指向包含其他信息的一个数据结构的指针。
创建多个线程,使这些线程拥有与起始点相同的函数地址,这是完全合乎逻辑的并且是非常有用的。例如,可以实现一个We b服务器,以便创建一个新线程来处理每个客户机的请求。每个线程都知道它正在处理哪个客户机的请求,因为当创建线程时,你传递了一个不同的p z P a r a m值。
记住,Wi n d o w s是个抢占式多线程系统,这意味着新线程和调用C r e a t e T h r e a d的线程可以同时执行。由于线程可以同时运行,就会出现一些问题。请看下面的代码:
DWORD WINAPI FirstThread(PVOID pvParam)
{
//Initialize a stack-based variable
int x = 0;
DWORD dwThreadID;
//Create a new thread.
HANDLE hThread = CreateThread(NULL, 0, SecondThread,
(PVOID)&x, 0, &dwThreadId);
//We don't reference the new thread anymore,
//so close our handle to it.
closeHandle(hThread);
//Our thread is done.
//BUG:our stack will be destroyed,
//but SecondThread might try to access it.
return(0);
}
DWORD WINAPI SecondThread(PVOID pvParam)
{
//Do some lengthy processing here.
...
//Attempt to access the variable on FirstThread's stack.
//NOTE:This may cause an access violation - it depends on timing!
*((int *) pvParam) = 5;
...
return(0);
}
在上面这个代码中,F i r s t T h r e a d可以在S e c o n d T h r e a d将5分配给F i r s t T h r e a d的x之前结束它的操作。如果出现这种情况,S e c o n d T h r e a d将不知道F i r s t T h r e a d已经不再存在,并且仍然试图修改现在已经是个无效地址的内容。这会导致S e c o n d T h r e a d产生一次访问违规,因为F i r s t T h r e a d的堆栈已经在F i r s t T h r e a d终止运行时被撤消。解决这个问题的方法之一是将x声明为一个静态变量,这样,编译器就为应用程序的数据部分中的x创建一个存储区,而不是在堆栈上创建存储区。
但是这使得函数成为不可重新进入的函数。换句话说,无法创建两个执行相同函数的线程,因为两个线程将共享该静态变量。解决这个问题(和它的更复杂的变形)的另一种方法是使用正确的线程同步技术(第8、9章和1 0章介绍)
6.4.4 fdwCreate
f d w C r e a t e参数可以设定用于控制创建线程的其他标志。它可以是两个值中的一个。如果该值是0,那么线程创建后可以立即进行调度。如果该值是C R E AT E _ S U S P E N D E D,系统可以完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。
C R E AT E _ S U S P E N D E D标志使得应用程序能够在它有机会执行任何代码之前修改线程的某些属性。由于这种必要性很少,因此该标志并不常用。第5章介绍的J o b L a b应用程序说明了该标志的正确方法。
6.4.5 pdwThreadID
C r e a t e T h r e a d的最后一个参数是p d w T h r e a d I D,它必须是D W O R D的一个有效地址,C r e a t e T h r e a d使用这个地址来存放系统分配给新线程的I D (进程和线程的I D已经在第4章中作了介绍)。
注意在Windows 2000(和Windows NT 4)下,可以(并且通常是这样做的)为该参数传递N U L L。它告诉函数,你对线程的I D不感兴趣,但是线程已经创建了。在Windows 95和Windows 98下,为该参数传递N U L L会导致函数运行失败,因为函数试图将I D写入地址N U L L(这是不合法的)。因此线程不能创建。
当然,操作系统之间的不一致现象会给编程人员带来一些问题。例如,在Wi n d o w s2 0 0 0下(即使为p d w T h r e a d I D参数传递了N U L L,它也创建了该线程)编写和测试了一个应用程序,当后来在Windows 98上运行该应用程序时,C r e a t e T h r e a d将不创建新的线程。必须始终在你声称支持的所有操作系统(和所有版本)上充分测试应用程序。
6.5 终止线程的运行
若要终止线程的运行,可以使用下面的方法:
• 线程函数返回(最好使用这种方法)。
• 通过调用E x i t T h r e a d函数,线程将自行撤消(最好不要使用这种方法)。
• 同一个进程或另一个进程中的线程调用Te r m i n a t e T h r e a d函数(应该避免使用这种方法)。
• 包含线程的进程终止运行(应该避免使用这种方法)。
下面将介绍终止线程运行的方法,并且说明线程终止运行时会出现什么情况。
6.5.1 线程函数返回
始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
• 在线程函数中创建的所有C + +对象均将通过它们的撤消函数正确地撤消。
• 操作系统将正确地释放线程堆栈使用的内存。
• 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
• 系统将递减线程内核对象的使用计数。
6.5.2 ExitThread函数
可以让线程调用E x i t T h r e a d函数,以便强制线程终止运行:
VOID ExitThread(DWORD dwExitCode);
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C + +资源(如C + +类对象)将不被撤消。由于这个原因,最好从线程函数返回,而不是通过调用E x i t T h r e a d来返回(详细说明参见第4章)。
当然,可以使用E x i t T h r e a d的d w E x i t T h r e a d参数告诉系统将线程的退出代码设置为什么。E x i t T h r e a d函数并不返回任何值,因为线程已经终止运行,不能执行更多的代码。
注意终止线程运行的最佳方法是让它的线程函数返回。但是,如果使用本节介绍的方法,应该知道E x i t T h r e a d函数是Wi n d o w s用来撤消线程的函数。如果编写C / C + +代码,那么决不应该调用E x i t T h r e a d。应该使用Visual C++运行期库函数_ e n d t h r e a d e x。如果不使用M i c r o s o f t的Visual C++编译器,你的编译器供应商有它自己的E x i t T h r e a d的替代函数。不管这个替代函数是什么,都必须使用。本章后面将说明_ e n d t h r e a d e x的作用和它的重要性。
6.5.3 TerminateThread函数
调用Te r m i n a t e T h r e a d函数也能够终止线程的运行:
BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode);
与E x i t T h r e a d不同,E x i t T h r e a d总是撤消调用的线程,而Te r m i n a t e T h r e a d能够撤消任何线程。h T h r e a d参数用于标识被终止运行的线程的句柄。当线程终止运行时,它的退出代码成为你作为d w E x i t C o d e参数传递的值。同时,线程的内核对象的使用计数也被递减。
注意Te r m i n a t e T h r e a d函数是异步运行的函数,也就是说,它告诉系统你想要线程终止运行,但是,当函数返回时,不能保证线程被撤消。如果需要确切地知道该线程已经终止运行,必须调用Wa i t F o r S i n g l e O b j e c t (第9章介绍)或者类似的函数,传递线程的句柄。
设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤消的通知。线程不能正确地清除,并且不能防止自己被撤消。
注意当使用返回或调用E x i t T h r e a d的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用Te r m i n a t e T h r e a d,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。M i c r o s o f t故意用这种方法来实现Te r m i n a t e T h r e a d。如果其他仍然正在执行的线程要引用强制撤消的线程堆栈上的值,那么其他的线程就会出现访问违规的问题。如果将已经撤消的线程的堆栈留在内存中,那么其他线程就可以继续很好地运行。
此外,当线程终止运行时, D L L通常接收通知。如果使用Terminate Thread 强迫线程终止,D L L就不接收通知,这能阻止适当的清除(详细信息参见第2 0章)
6.5.4 在进程终止运行时撤消线程
第4章介绍的E x i t P r o c e s s和Te r m i n a t e P r o c e s s函数也可以用来终止线程的运行。差别在于这些线程将会使终止运行的进程中的所有线程全部终止运行。另外,由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。这当然包括所有线程的堆栈。这两个函数会导致进程中的剩余线程被强制撤消,就像从每个剩余的线程调用Te r m i n a t e T h r e a d一样。显然,这意味着正确的应用程序清除没有发生,即C + +对象撤消函数没有被调用,数据没有转至磁盘等等。
6.5.5 线程终止运行时发生的操作
当线程终止运行时,会发生下列操作:
• 线程拥有的所有用户对象均被释放。在Wi n d o w s中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。
• 线程的退出代码从S T I L L _ A C T I V E改为传递给E x i t T h r e a d或Te r m i n a t e T h r e a d的代码。
• 线程内核对象的状态变为已通知。
• 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。
• 线程内核对象的使用计数递减1。
当一个线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。
一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调用G e t E x i t c o d e T h r e a d来检查由h T h r e a d标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码:
BOOL GetExitCodeThread(
HANDLE hThread,
PDWORD pdwExitCode);
退出代码的值在p d w E x i t C o d e指向的D W O R D中返回。如果调用G e t E x i t C o d e T h r e a d时线程尚未终止运行,该函数就用S T I L L _ A C T I V E标识符(定义为0 x 1 0 3)填入D W O R D。如果该函数运行成功,便返回T R U E(第9章将详细地介绍如何使用线程的句柄来确定何时线程终止运行)。