线程基础知识
——Windows核心编程学习手札系列之六
线程与进程一样由两部分构成:一是线程的内核对象,操作系统用它来对线程实施管理,也是系统用来存放线程统计信息的地方;二是线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量。进程是静止的,不执行代码,只是线程的容器,线程总是在某个进程环境中创建的,且其整个寿命周期都在该进程中,线程在进程的地址空间中执行代码和操作数据。在单进程中,存在多个线程运行,这些线程共享进程的地址空间,执行相同代码和操作相同数据,还共享内核对象句柄(句柄表依赖于进程存在而非线程)。进程使用的资源比较多,需要为进程创建一个虚拟地址空间(需要系统很多资源);要保留众多记录(占用大量内存);文件如exe和dll要加载到地址空间(需要文件资源),相比下线程只需要一个内核对象和一个堆栈,需要少量内存维护记录即可。线程的开销比进程少,在开发中始终设法用线程来解决编程问题是最优选择。
每个进程在初始化时,系统会为其创建一个主线程,该线程与C/C++运行期库的启动代码一并开始运行,启动代码调用进入点函数(main/wmain/WinMain/wWinMian)并继续运行直到进入点函数返回并且C/C++运行期库的启动代码调用ExitProcess为止。每个线程都有进入点函数,线程从这个进入点开始运行,主线程的进入点函数是main/wmain/WinMain/wWinMain,如想要在进程中创建辅助线程,也需要一个进入点函数,如同下面的代码:
DWORD WINAPI ThreadFunc(PVOID pvParam){
DWORD dwResult=0;
……
return (dwResult);
}
线程函数可以执行你所要执行的任务,线程函数达到结尾处且返回,线程终止运行,堆栈的内存被释放,同时线程的内核对象被递减,如使用计数为零,其内核对象被撤消。对线程函数应该认识到的是:1)主线程的进入点函数名字必须是main/wmain/wWinMain/WinMain这四个,其他线程函数可以使用任何名字,但不同线程之间的函数名必须不同,否则编译器或链接程序会认为单个函数创建了多个实现函数;2)线程函数可以传递单个参数,参数含义自定义而不由操作系统来定义;3)线程函数必须返回一个值,作为该线程的推出代码;4)线程函数应尽可能使用函数参数和局部变量,如使用静态变量和全局变量,多个线程可以同时访问这些变量,可能破坏变量的内容。
创建新的线程,可在正运行的线程中调用CreateThread函数来实现:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadID);
当CreateThread被调用时,系统创建一个线程内核对象,是操作系统用来管理线程的较小数据结构,是关于线程统计信息组成的小型数据结构。系统从进程的地址空间中分配内存供线程的堆栈使用,新线程运行的进程环境与创建线程一样,新线程可以访问进程的内核对象的所有句柄、所有内存和在这个进程中的其他线程的堆栈。CreateThread函数是Windows提供的用来创建线程,如在编写C/C++代码,不应调用该函数,而应该使用Visual C++运行期库函数_beginthreadex来创建新线程。一个原则就是尽量采用编译器供应商自己的创建线程函数来代替CreateThread的Windows函数。
函数CreateThread的第一个参数psa是指向SECURITY_ATTRIBUTES结构的指针,如仅设置该线程的内核对象为默认安全属性,可以直接传递NULL;如希望子进程能够继承该线程对象的句柄,则需要设定一个SECURITY_ATTRIBUTES结构,它的bInheritHandle成员被初始化为TURE值。
函数CreateThread的第二个参数cbStack用于设定线程可以将多少地址空间用于自己的堆栈。当CreateProcess启动一个进程时,在内部调用CreateThread来对进程的主线程进行初始化。对于cbStack参数来说,CreateProcess使用存在在可执行文件中的一个值,可使用链接程序的/STACK开关来控制cbStack值:/STACK:[reverse][.commit],reserve参数用于设定系统应该为线程堆栈保留的地址空间量,默认值是1MB;commit参数用于设定开始时应该承诺用于堆栈保留区的物理存储器的容量,默认值是 1页。当线程中的代码执行时,可能需要多个页面的存储器,当线程溢出它的堆栈时,就生成一个异常条件,系统抓取该异常条件,并将另一页用于保留空间,使得线程的堆栈能够根据需要动态地扩大。当调用CraeteThrad时,如果cbStack传递的值不是0,就能使该函数将所有的存储器保留并分配给线程的堆栈,由于所有的存储器预先作了分配,因此可以确保线程拥有指定容量的可用堆栈存储器。保留空间的容量可以在是/STACK链接设定的值也可以是cbStack的值,谁大用谁。分配的存储器容量则与传递的cbStack一致,如将0传递给cbStack参数,CreateThread 就保留一个区域,并且将链接程序嵌入exe文件的/STACk链接程序开关信息指明的存储器容量分配给线程堆栈。保留空间的容量用于为堆栈设置一个上限,这样可以抓住代码中的循环递归错误,防止递归函数无限制地循环调用自己和创建新堆栈,从而消耗掉存储资源。
函数CreateThread的第三个参数pfnStartAddr指明想要新线程执行的线程函数地址。函数CreateThread的第四个参数pvParam作为将初始化值传递给线程函数的手段,既可以是数字值,也可以是指向包含其他信息的一个数据结构指针。创建多个线程,使这些线程拥有和起始点相同的函数地址,如实现一个Web服务器,创建一个新线程处理每个客户机的请求,每个线程都知道自己正在服务于哪个客户端的请求,这需要在创建线程时传递不同的pvParam参数。
函数CreateThread的第五个参数fdwCreate可以设定用于控制创建线程的其他标志,如果该值是0,那线程创建后立即进行调度,如该值是CREATE_SUSPENDED,系统可以完整地创建线程并对它进行初始化,但要暂停该线程的运行,使其无法进行调度。CREATE_SUSPENDED标志使得应用程序能够在它有机会执行任何代码之前修改线程的某些属性,由于这种必要性很少,所以通常不用。函数CreateThread的第六个参数pdwThreadID是DWORD类型的一个有效地址,CreateThread使用这个地址来存放系统分配给新线程的ID。
终止线程的运行,方法有:1)线程函数返回(最优);2)调用ExitThread函数,线程将自行撤消(最好不用);3)同一个进程或另一个进程中的线程调用TerminateThread函数(避免使用该方法);4)包含线程的进程终止运行(避免使用)。线程函数返回可确保所有线程资源被正确地清除:1)在线程函数中创建的所有C++对象均将通过它们的撤消函数正确地撤消;2)操作系统将正确地释放线程堆栈使用的内存;3)系统将线程的推出代码(在线程的内核对象中维护)设置为线程函数的返回值;4)系统将递减线程内核对象的使用计数。可以让线程强制终止运行,通过调用VOID ExitThread(DWORD dwExitCode)函数来实现,该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源,但C++资源如C++类对象将不被撤消,基于这个原因,最好从线程函数返回,而不是通过调用ExitThread函数来返回。如创建线程一样,应尽量使用Visual C++运行期库函数_endthreadex来终止线程运行。TerminateThread函数也可终止线程运行:BOOL TerminateThread(HANDLE hThread,DOWRD dwExitCode),能够撤消任何线程而ExitThread只能撤消调用的线程,其中hThread作为参数用于标识被终止运行线程的句柄。TerminateThread函数是异步运行的函数,告诉系统终止线程的运行,但当函数返回时,不能保证线程被撤消,如果需要确切知道线程终止运行,需调用WaitForSingleObject或者类似函数,传递线程的句柄。当线程终止运行时,会发生下列操作:1)线程拥有的所有用户对象均被释放,在Windows中大多数对象都是由包含创建这些对象的线程的进程所拥有的,但是一个线程拥有两个用户对象,即窗口和挂钩,当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩,其他对象只有在拥有线程的进程终止运行时才被撤消;2)线程的退出代码从STILL_ACTIVE改为传递给ExitThread或TerminateThread的代码;3)线程内核对象状态变为已通知;4)如果线程是进程中的最后一个活动线程,系统也将进程视为已终止运行;5)线程内核对象的使用计数递减1。一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄,然而别的线程可以用GetExitCodeThread来检查由hThread标识的线程是否已经终止运行,如果它已经终止运行则确定它的退出代码:BOOL GetExitCodeThread(HANDLE hThread,PDWORD pdwExitCode),退出代码的值在pdwExitCode指向的DWORD中返回,如果调用GetExitCodeThread函数时线程还未终止运行,该函数就用STILL_ACTIVE标识符(定义为0x103)填入DWORD,如果函数运行成功便返回TURE值。
如非 2008-12-16