第6章线程的基础知识理解线程是非常关键的,因为每个进程至少需要一个线程。本章将更加详细地介绍线程的知识。尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用。还要介绍系统如何使用线程内核对象来管理线程。与进程内核对象一样,线程内核对象也拥有属性,我们将要观察许多用于查询和修改这些属性的函数。此外还要介绍可以在进程中创建和生成更多的线程时所用的函数。第4章介绍了进程是由两个部分构成的,一个是进程内核对象,另一个是地址空间。同样,线程也是由两个部分组成的:•一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。•另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量(第16章将进一步介绍系统如何管理线程堆栈)。第4章中讲过,进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。如你所见,进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间需要许多系统资源。系统中要保留大量的记录,这要占用大量的内存。另外,由于.exe和.dll文件要加载到一个地址空间,因此也需要文件资源。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。由于线程需要的开销比进程少,因此始终都应该设法用增加线程来解决编程问题,而要避免创建新的进程。但是,这个建议并不是一成不变的。许多程序设计用多个进程来实现会更好些。应该懂得权衡利弊,经验会指导你的编程实践。在详细介绍线程之前,首先花一点时间讲一讲如何正确地在应用程序结构中使用线程。6.1何时创建线程线程用于描述进程中的运行路径。每当进程被初始化时,系统就要创建一个主线程。该线程与C/C++运行期库的启动代码一道开始运行,启动代码则调用进入点函数(main、wmain、WinMain或wWinMain),并且继续运行直到进入点函数返回并且C/C++运行期库的启动代码调用ExitProcess为止。对于许多应用程序来说,这个主线程是应用程序需要的唯一线程。不过,进程能够创建更多的线程来帮助执行它们的操作。每个计算机都拥有一个功能非常强大的资源,即CPU。让CPU闲置起来是绝对没有道理的(如果忽略节省电能问题的话)。为了使CPU处于繁忙状态之中,可以让它执行各种不同的工作。下面是一些例子:•可以打开MicrosoftWindows2000配备的内容索引服务程序。它能够创建一个低优先级的线程,以便定期打开你的磁盘驱动器上的文件内容并给内容做索引。若要找到一个文件,可以打开SearchResult(搜索结果)窗口(方法是单击Start按钮,从Search菜单中选定ForFilesOrFolders),再将你的搜索条件输入ContainingText域。这时就可以搜索到索引,相关的文件就会立即显示出来。内容索引服务程序大大改进了性能,因为每次搜索不必打开、扫描和关闭磁盘驱动器上的每个文件。•可以使用Windows2000配备的磁盘碎片整理软件。通常情况下,这种类型的实用程序拥有许多管理选项,一般用户可能不懂,比如该实用程序应该相隔多长时间运行一次,何时运行。使用低优先级线程,可以在后台运行该实用程序,并且在系统空闲时对驱动器进行碎片整理。•可以很容易地设想将来版本的编译器,每当暂停键入时,它就可以自动编译你的源代码文件。输出窗口可以向你(几乎)实时显示警告和出错信息。当键入变量和函数名时出现错误时,就能立即发现。在某种程度上讲,MicrosoftVisualStudio已经实现了这个功能,使用Workspace的ClassView窗格,就能够看到这些信息。•电子表格应用程序能够在后台执行各种计算。•字处理程序能够执行重新分页、拼写和语法检查及在后台进行打印。•文件可以在后台拷贝到其他介质中。•Web浏览器在后台与它们的服务器进行通信。因此,在来自当前Web站点的结果输入之前,用户可以缩放浏览器的窗口或者转到另一个Web站点。这些例子中,有一个重要问题应该注意,那就是多线程能够简化应用程序的用户界面。如果每当停止键入时,编译器建立了你的应用程序,那么就没有必要提供Build菜单选项。文字处理应用程序不需要CheckSpelling(拼写检查)和CheckGrammar(语法检查)菜单选项。在Web浏览器的例子中,注意,将不同的线程用于I/O(网络、文件或其他),应用程序的用户界面就能够始终保持工作状态。比如有一个应用程序负责给数据库记录进行排序、打印文档或拷贝文件。如果将独立的线程用于处理这个与I/O相关的任务,用户就可以在进程中继续使用应用程序界面来取消操作。设计一个拥有多线程的应用程序,就会扩大该应用程序的功能。我们在下一章中可以看到,每个线程被分配了一个CPU。因此,如果你的计算机拥有两个CPU,你的应用程序中有两个线程,那么两个CPU都将处于繁忙状态。实际上,你是让两个任务在执行一个任务的时间内完成操作。每个进程至少拥有一个线程。因此,如果你在应用程序中不执行任何特殊的操作,在多进程操作系统上运行,就能够得到许多好处。例如,可以建立一个应用程序,并同时使用文字处理程序(我常常这样做)。如果计算机拥有两个CPU,那么该应用程序就可以在一个处理器上执行,而另一个处理器则负责处理文档。另外,如果编译器出现一个错误,导致它的线程进入一个无限循环,仍然可以使用其他的进程(16位Windows和MS-DOS应用程序则不行)6.2何时不能创建线程至今为止,一直在讨论多线程应用程序的优点。虽然多线程应用程序的优点很多,但是它也存在某些不足之处。有些开发人员认为,解决问题的方法是将它分割成多个线程。这种想法是完全错误的。线程确实是非常有用的,但是,当使用线程时,在解决原有的问题时可能产生新的问题。例如,你开发了一个文字处理应用程序,并且想要让打印函数作为它自己的线程来运行。这听起来是个很好的主意,因为用户可以在打印文档时立即回头着手编辑文档。但是,这意味着文档中的数据可能在文档打印时变更。也许最好是不要让打印操作在它自己的线程中发生,不过这种“方案”看起来有点儿极端。如果你让用户编辑另一个文档,但是锁定正在打印的文档,使得打印结束前该文档不能修改,那将会怎样呢?这里还有第三种思路,将文档拷贝到一个临时文件,然后打印该临时文件的内容,并让用户修改原始文档。当包含该文档的临时文件结束打印时,删除临时文件。如你所见,线程能够解决某些问题,但是却又会产生新的问题。在开发应用程序的用户界面时,很可能出现对线程的另一种误用。几乎在所有的应用程序中,所有用户界面的组件(窗口)应该共享同一个线程。单个线程应该创建窗口的所有子窗口。有时在不同的线程上创建不同的窗口是有用的,不过这种情况确实非常少见。通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个GetMessage循环。进程中的所有其他线程都是工作线程,它们与计算机或I/O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。虽然单个进程拥有多个用户界面线程的情况并不多见,但是这种情况有着某种有效的用途。WindowsExplorer为每个文件夹窗口创建了一个独立的线程。它使你能够将文件从一个文件夹拷贝到另一个文件夹,并且仍然可以查看你的系统上的其他文件夹。另外,如果Explorer中存在一个错误,那么负责处理文件夹的线程可能崩溃,但是仍然能够对其他文件夹进行操作,至少在执行的操作导致其他文件夹也崩溃之前,仍然可以对它们进行操作(关于线程和用户界面的详细说明,参见第26和27章)。上述内容的实质是应该慎重地使用多线程。不要想用就用。仅仅使用赋予进程的主线程,就能够编写出许多非常有用的和功能强大的应用程序。6.3编写第一个线程函数每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。前面已经介绍了主线程的进入点函数:即main、wmain、WinMain或wWinMain。如果想要在你的进程中创建一个辅助线程,它必定也是个进入点函数,类似下面的样子:DWORDWINAPIThreadFunc(PVOIDpvParam){DWORDdwResult=0;...return(dwResult);}你的线程函数可以执行你想要它做的任何任务。最终,线程函数到达它的结尾处并且返回。这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果使用计数降为0,线程的内核对象就被撤消。与进程内核对象的情况相同,线程内核对象的寿命至少可以达到它们相关联的线程那样长,不过,该对象的寿命可以远远超过线程本身的寿命。下面对线程函数的几个问题作一说明:•主线程的进入点函数的名字必须是main、wmain、WinMain或wWinMain,与这些函数不同的是,线程函数可以使用任何名字。实际上,如果在应用程序中拥有多个线程函数,必须为它们赋予不同的名字,否则编译器/链接程序会认为你为单个函数创建了多个实现函数。•由于给你的主线程的进入点函数传递了字符串参数,因此可以使用ANSI/Unicode版本的进入点函数:main/wmain和WinMain/wWinMain。可以给线程函数传递单个参数,参数的含义由你而不是由操作系统来定义。因此,不必担心ANSI/Unicode问题。•线程函数必须返回一个值,它将成为该线程的退出代码。这与C/C++运行期库关于让主线程的退出代码作为进程的退出代码的原则是相似的。•线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,这可能破坏变量的内容。然而,参数和局部变量是在线程堆栈中创建的,因此它们不太可能被另一个线程破坏。既然懂得了实现线程函数的方法,下面讲述如何让操作系统来创建能够执行线程函数的线程。6.4CreateThread函数前面已经讲述了调用CreateProcess函数时如何创建进程的主线程。如果想要创建一个或多个辅助函数,只需要让一个已经在运行的线程来调用CreateThread:HANDLECreateThread(PSECURITY_ATTRIBUTESpsa,DWORDcbStack,PTHREAD_START_ROUTINEpfnStartAddr,PVOIDpvParam,DWORDfdwCreate,PDWORDpdwThreadID);当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。这与进程和进程内核对象之间的关系是相同的。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。注意CreateThread函数是用来创建线程的Windows函数。不过,如果你正在编写C/C++代码,决不应该调用CreateThread。相反,应该使用VisualC++运行期库函数_beginthreadex。如果不使用Microsoft的VisualC++编译器,你的编译器供应商有它自己的CreateThred替代函数。不管这个替代函数是什么,你都必须使用。本章后面将要介绍_beginthreadex能够做什么,它的重要性何在。这就是Create