windows核心编程6

来源:互联网 发布:淘宝秒刷销量 编辑:程序博客网 时间:2024/06/05 04:31
 
第6章线程的基础知识
进程是由两个部分构成的,一个是进程内核对象,另一个是地址空间。同样,线程也是由两个部分组成的:
* 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
* 另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量(第16章将进一步介绍系统如何管理线程堆栈)。
 
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。如你所见,进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间需要许多系统资源。系统中要保留大量的记录,这要占用大量的内存。另外,由于. e x e. d l l文件要加载到一个地址空间,因此也需要文件资源。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。由于线程需要的开销比进程少,因此始终都应该设法用增加线程来解决编程问题,而要避免创建新的进程。但是,这个建议并不是一成不变的。许多程序设计用多个进程来实现会更好些。应该懂得权衡利弊,经验会指导你的编程实践。
 
6.1 何时创建线程
线程用于描述进程中的运行路径。每当进程被初始化时,系统就要创建一个主线程。该线程与C / C + +运行期库的启动代码一道开始运行,启动代码则调用进入点函数(main、wmain、WinMain或wWinMain),并且继续运行直到进入点函数返回并且C / C + +运行期库的启动代码调用E x i t P r o c e s s为止。
 
6.2 何时不能创建线程
通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个G e t M e s s a g e循环。进程中的所有其他线程都是工作线程,它们与计算机或I / O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。
 
6.3 编写第一个线程函数
你的线程函数可以执行你想要它做的任何任务。最终,线程函数到达它的结尾处并且返回。这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果使用计数降为0,线程的内核对象就被撤消。与进程内核对象的情况相同,线程内核对象的寿命至少可以达到它们相关联的线程那样长,不过,该对象的寿命可以远远超过线程本身的寿命。
 
下面对线程函数的几个问题作一说明:
* 主线程的进入点函数的名字必须是main、wmain、WinMain或wWinMain,与这些函数不同的是,线程函数可以使用任何名字。
* 由于给你的主线程的进入点函数传递了字符串参数,因此可以使用ANSI/Unicode版本的进入点函数: main/wmain和WinMain/wWinMain。可以给线程函数传递单个参数,参数的含义由你而不是由操作系统来定义。因此,不必担心ANSI/Unicode问题。
* 线程函数必须返回一个值,它将成为该线程的退出代码。这与C / C + +运行期库关于让主线程的退出代码作为进程的退出代码的原则是相似的。
* 线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,这可能破坏变量的内容。然而,参数和局部变量是在线程堆栈中创建的,因此它们不太可能被另一个线程破坏。既然懂得了实现线程函数的方法,下面讲述如何让操作系统来创建能够执行线程函数。
 
6.4 CreateThread函数
CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。这与进程和进程内核对象之间的关系是相同的。
 
系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
注意CreateThread函数是用来创建线程的Windows函数。不过,如果你正在编写C / C + +代码,决不应该调CreateThread。相反,应该使用Visual C++运行期库函数_be ginthreadex。如果不使用Microsoft的Visual C++编译器,你的编译器供应商有它自己的CreateThred替代函数。不管这个替代函数是什么,你都必须使用。
 
6.5 终止线程的运行
若要终止线程的运行,可以使用下面的方法:
* 线程函数返回(最好使用这种方法)。
* 通过调用E x i t T h r e a d函数,线程将自行撤消(最好不要使用这种方法)。
* 同一个进程或另一个进程中的线程调用TerminateThread函数(应该避免使用这种方法)。
* 包含线程的进程终止运行(应该避免使用这种方法)。
 
下面将介绍终止线程运行的方法,并且说明线程终止运行时会出现什么情况。
 
6.5.1 线程函数返回
始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
* 在线程函数中创建的所有C + +对象均将通过它们的撤消函数正确地撤消。
* 操作系统将正确地释放线程堆栈使用的内存。
* 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
* 系统将递减线程内核对象的使用计数。
 
6.5.2 ExitThread函数
可以让线程调用E x i t T h r e a d函数,以便强制线程终止运行。
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,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函数并不返回任何值,因为线程已经终止运行,不能执行更多的代码。注意终止线程运行的最佳方法是让它的线程函数返回。但是,如果使用本节介绍的方法,应该知道ExitThread函数是Windows用来撤消线程的函数。如果编写C/C++代码,那么决不应该调用ExitThread。应该使用Visual C++运行期库函数_endthreadex。如果不使用Microsoft的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 Te r m i n a t e T h r e a d函数
调用Te r m i n a t e 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总是撤消调用的线程,而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参数传递的值。同时,线程的内核对象的使用计数也被递减。
 
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标识的线程是否已经终止运行。
 
6.6 线程的一些性质
系统在创建线程和对线程进行初始化时必须做些什么工作?
调用C r e a t e T h r e a d可使系统创建一个线程内核对象。该对象的初始使用计数是2(在线程停止运行和从C r e a t e T h r e a d返回的句柄关闭之前,线程内核对象不会被撤消)。线程的内核对象的其他属性也被初始化,暂停计数被设置为1,退出代码始终为S T I L L _ A C T I V E0 x 1 0 3),该对象设置为未通知状态。一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间分配而来的,因为线程并不拥有它自己的地址空间。每个线程都有它自己的一组C P U寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的C P U寄存器的状态。线程的这组C P U寄存器保存在一个C O N T E X T结构(在Wi n N T. h头文件中作了定义)中。C O N T E X T结构本身则包含在线程的内核对象中。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。记住,线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。指令指针被置为线程实际开始执行的地方。
 
6.7 C/C++运行期库的考虑
 
Visual C++配有6个C / C + +运行期库。
库名                                        描述
L i b C . l i b       用于单线程应用程序的静态链接库(当创建新应用程序时,它是默认库)
L i b C D . l i b           用于单线程应用程序的静态链接库的调试版
L i b C M t . l i b          用于多线程应用程序的静态链接库的发行版
L i b CMt D . l i b         用于多线程应用程序的静态链接库的调试版
M S V C R t . l i b        用于动态链接M S V C R t . d l l库的发行版的输入库
M S V C R t D . l i b      用于动态链接M S V C R t D . d l l的调试版的输入库。该库同时支 持单线程应用程序和多线程应用程序
 
当实现任何类型的编程项目时,必须知道将哪个库与你的项目相链接。可以使用Project Settings对话框来选定一个库。在C / C + +选项卡上,在Code Generation(生成的代码)类别中,从Use run-time library(使用运行期库)组合框中选定6个选项中的一个。
 
如果创建一个多线程应用程序,必须在编译器的命令行上设定/ M T(指多线程应用程序)或/ M D(指多线程D L L)开关。这将使编译器能够定义_ M T标识符。
 
(_ b e g i n t h r e a d e x能够做什么,它的重要性何在?这里略)