读Windows核心编程 - 6

        线程由两部分构成:线程堆栈+线程内核对象。通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个GetMessage循环。进程中的所有其他线程都是工作线程,它们与计算机或者I/O相关联,但是这些线程从不创建窗口。虽然单个进程拥有多个用户界面线程的情况并不多见,但是这种情况有着某种有效的用途。Windows Explorer为每个文件夹窗口创建了一个独立的线程。它使你能够将文件从一个文件夹拷贝到另一个文件夹,并且仍然可以查看你的系统上的其他文件夹。

CreateThread:

HANDLE CreateThread(
     PSECURITY_ATTRIBUTES psa,                        
// 安全信息,同进程
     DWORD cbStack,       // 线程堆栈大小,默认1M,也可以通过链接开关/STACK:[reserve] [,commit]设定,谁大取谁
     PTHERAD_START_ROUTINE pfnStartAddr,          // 线程函数地址
     PVOID pvParam,                   // 传递给线程函数的参数
     DWORD fdwCreate,          // 如果是0,立即调度,还可以是CREATE_SUSPENDED(不常用)
     PDWORD pdwThreadID);            // 返回的线程ID

         有一个问题需要注意,如果线程A创建了线程B,并把局部变量a(一个int类型)的地址传给了B,在B中这么使用a: *(int *)pvParam = 1,可能就出错。因为在B运行到这句代码前A线程可能已经运行结束。解决方法可以使用一个静态变量。但是这使得函数成为不可重复进入的函数。换句话说,无法创建两个执行相同函数的线程,因为两个线程将共享该静态变量。解决这个问题的另一种更好的办法是使用正确的线程同步技术。

终止线程的运行

若要终止一个线程的运行,可以使用下面几种方法:

1. 线程函数返回(最好)

        这个方法是确保所有线程资源能正确的清楚的唯一方法。这个方法确保下面事项的实现:

        1. 在线程函数中创建的所有C++对象均被它们的撤销函数正确的撤销。

        2. 操作系统将正确的释放线程堆栈使用的内存。

        3. 系统将线程的退出代码设置为线程的返回值。

        4. 系统将递减线程内核对象的引用计数。

2. 调用ExitThread(避免使用)

        这种方法将导致C++资源不被撤销。但是线程堆栈能正确的被撤销。

3. 在同一个进程或者另外一个进程的线程中调用TerminateThread(避免使用)

        与ExitThread不同,ExitThread只能撤销调用的线程,而TerminateThread能够撤销任何线程。TerminateThread也是一个异步函数,也就是说,调用了以后不能保证线程被撤销。如果需要确切地知道线程被撤销,可以调用WaitForSingleObject或者类似的函数。使用TerminateThread不能清理C++资源,也不能撤销被撤销线程的堆栈,直到进程终止。因为其他现在在调用TerminateThread之后还可能使用被撤销线程堆栈上的值。当线程终止运行时,DLL通常收到通知。如果使用TerminateThread,DLL接受不到通知。

4. 包含线程的进程终止

线程终止时发生的操作:

1. 线程拥有的用户对象被释放。在windows中,大多数对象由进程拥有,但是一个线程有两个用户对象:即窗口和挂钩。当线程终止时,系统自动撤销任何窗口,并且卸载线程创建或安装的挂钩。其他对象只有当进程终止时才能被撤销。

2. 线程的退出代码从STILL_ACTIVE改为传递给ExitThread或者TerminateThread的代码。

3. 线程内核对象的状态变为已通知。

4. 如果是最后一个活动线程就终止进程。

5. 线程内核对象减1。

线程是如何工作的:

        当调用CreateThread的时候,首先创造了一个内核对象,该对象引用计数为2(在线程停止运行和从CreateThread返回的句柄之前,线程对象不会被撤销),暂停计数为1(当线程完全初始化后,系统查看CREATE_SUSPENDED标志是否传递给CreateThread,如果没有,暂停计数变为0,意味着该线程可以调度到一个进程中),退出代码始终为STILL_ACTIVE,通知状态为未通知状态。一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。然后系统将两个值写入堆栈的最上端。从上往写依次写入pvParam和pfnStartAddrj。

        每个线程都有它自己的一组寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的CPU寄存器的状态。线程的这组CPU寄存器保存在一个CONTEXT结构中。该结构本身则包含在线程的内核对象中。

        指令指针和堆栈指针寄存器是线程上下文中最重要的两个寄存器。当线程对象被初始化时,CONTEXT结构的堆栈指针寄存器被设置为线程堆栈上用来放置pfnStartAddr的地址。指令指针寄存器被置为BaseThreadStart函数的地址中(该函数未被文档化,包含在Kernel32.dll中)。该函数的定义如下:

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
     __try
{
                  ExitThread((pfnStartAddr)(pvParam));
     }

     __except(UnhandledExceptionFilter(GetExceptionInformation()))
{
                  ExitProcess(GetExceptionCode());
     }

}

         这个函数看起来有两个参数,好像是被另外的函数调用的,因为它可以访问这两个参数。其实不是,新线程只是在此处产生并且开始执行,之所以可以访问这两个参数是因为操作系统显示地将这两个参数写入了堆栈。当新线程执行BaseThreadStart函数时,将会出现下列情况:

1. 在线程函数中建立一个结构化异常处理帧,这样在线程执行时产生的任何异常都可以得到系统的默认处理。

2. 系统调用这个函数时,将你传递给它的pvParam参数传递给它。

3. 线程函数返回时,BaseThreadStart调用ExitThread,并将线程函数的返回值传给它。该线程内核对象引用计数递减。

4. 如果线程产生一个没有处理的异常,由BaseThreadStart函数建立的SEH帧将负责处理该异常条件。通常情况下,这意味着向用户显示一个对话框,并在用户撤销该消息框时BaseThreadStart调用ExitProcess终止进程。

        注意,线程总是在BaseThreadStart函数中被撤销,而不能退出该函数,这也是之所以返回void的原因。另外,在BaseThreadStart调用线程函数时,会将线程函数的返回地址写入堆栈,这样线程函数就知道何处返回。而BaseThreadStart本身不允许返回,因为堆栈上根本没有返回地址。

        当主线程初始化时,情况与上面描述的类似。它的指令指针被设置为另一个未文档化的函数,这个函数是BaseProcessStart。与BaseThreadStart函数唯一的不同是,BaseProcessStart没有参数。当BaseProcessStart开始执行时,它调用C/C++运行期的启动代码。主线程从不返回BaseProcessStart函数。

C/C++运行期库的考虑:

        Visual C++配有6个C/C++运行期库。分别是单线程静态发行版,单线程调静态试版,多线程静态发行版,多线程静态调试版,动态链接库发行版,动态链接库调试版(支持单线程和多线程)。在出现C运行期库的时候还没有多线程的概念,所以有很多设计不能满足多线程程序,最简单的例子就是C运行期库中的全局变量errno,一个线程对这个变量的改变会响应到其他线程的errno值,这不是我们所期望的。若要使C和C++程序能够正确地运行,必须创建一个数据结构,并将它与使用C/C++运行期库函数的每个线程关联起来。然而系统是不知道在创建新线程时应该分配该数据块的,因此如果在C/C++中直接用CreateThread来创建新线程是错误的,必须调用C/C++运行期函数_beginthreadex。它的参数列表与CreateThread是相同的,但是参数名和类型并不完全相同。而且_beginthreadex只存在与C/C++运行期库的多线程版本中。如果链接到单线程运行期库,就会出现一个链接错误。Visual Studio默认选定单线程库。现在来看一下_beginthreadex做了些什么工作:

1. 在堆上分配一个tiddata数据块,并将线程函数地址pfnStartAddr和参数pvParam传给tiddata中的两个成员。

2. 在_beginthreadex内部调用CreateThread函数,这个操作系统了解如果创建新线程的唯一方法。

3. 在调用CreateThread时,传给它的函数地址是_threadstartex,参数地址是tiddata的地址。

接下去的任务是观察_threadstartex函数为我们做了什么:

1. 由于调用了CreateThread,新线程先用BasethreadStart开始执行,然后转到_threadstartex。

2. 调用TlsSetValue(__tlsindex, ptd); TlsSetValue是个系统函数,负责将一个值与调用线程联系起来。这称为线程本地储存器(TLS),将在第21章介绍。_threadstartex函数将tiddata块与线程联系起来。

3. 一个SEH帧被放置在需要的线程函数周围。这个帧负责处理与运行期相关的许多事情。例如:运行期错误(比如放过了没有抓住的C++异常条件)和C/C++运行期库的signal函数。如果用CreateThread创建线程,那么signal函数就不能正常运行。

4. 调用必要的线程函数,传递正确的参数。线程函数地址和参数地址都已经保存在tiddata结构体中,如下所示:......(((_ptiddata)ptd)->_initaddr)(((_ptidata)ptd)->_initarg)

5. 必要的线程函数的返回值认为是线程的退出代码。注意,_threadstartex不只是返回到BaseThreadStart。如果它这样做,那么tiddata的内存块不能得到释放,因此可以调用另一个函数_endthreadex,并传递退出代码。如下:_endthreadex(.....(((_ptiddata)ptd)->_initaddr)(((_ptidata)ptd)->_initarg));

在_endthreadex函数中,首先调用ptd = _gettd(); 该函数调用TlsGetValue检索调用线程的tiddata地址,然后free该内存块,最后调用ExitThread撤销该线程,当然,退出代码要正确的设置和传递。

本章前面提到过,应该避免使用ExitThread,因为ExitThread函数将撤销调用函数,并且不允许它从当前函数返回。由于该函数不能返回,所以创建的C++对象都不能被撤销。另一个原因是,调用ExitThread不能撤销tiddata数据块。如果一定要使用,可以使用_endthreadex,它可以撤销tiddata内存块,但是也不推荐使用。

        在tiddata中我们存放的是与线程相关的一些数据,比如errno。一旦数据块被初始化并且与线程联系起来,线程调用的任何需要单线程实例数据的C/C++运行期库函数都能很容易地(通过TlsGetValue)检索调用线程的数据块地址,并对线程的数据进行操作。这对于函数来说很好,但是对errno这样的全局变量该怎么处理呢?我们知道这个变量已经在tiddata中存在一份与每个线程相关的拷贝,在多线程环境下当引用errno时,其实是调用了C/C++运行期函数_errno。该函数将返回线程相关数据块中的errno数据成员的地址。

        也许你想知道,如果调用了CreateThread而不是调用_beginthreadex会出现什么效果。大多数C/C++运行期函数是线程安全的,当一个线程调用一个需要tiddata结构的C/C++运行期函数时,首先C/C++运行期函数试图通过TlsGetValue获得线程数据块的地址,如果返回NULL,C/C++运行期函数就在现场为调用线程分配一个tiddata块,并对它进行初始化。然后tiddata通过TlsSetValue与线程相关联。乍一看似乎没什么问题,但是如果调用signal这样的函数整个进程就会终止。因为结构化异常处理帧没有准备好。第二,没人会用CreateThread创建线程用_endthreadex撤销线程,那么tiddata块也得不到释放。

        另外,在C/C++运行期库中还提供了_beginthread和_endthread函数,但是这两个函数参数都很少,因此比特性全面的_beginthreadex和_endthreadex函数受到很大的限制。例如,如果使用_beginthread,就无法创建带有安全性属性的新线程,也无法创建暂停线程,也无法获得线程的ID值等等。

        当线程运行时,它们常常想要调用Windows函数来改变它们的运行环境。Windows提供了一些函数,使线程能够很容易引用它的进程内核对象或者线程内核对象:GetCurrentProcess&GetCurrentThread。这两个函数返回的都是伪句柄,如果调用CloseHandle来传递,那么CloseHandle就会忽略该参数的调用并返回FALSE。当调用一个需要进程句柄或线程句柄的windows函数时,可以传递一个伪句柄,使该函数执行它对调用进程或线程的操作。例如,可以把伪句柄传递给GetProcessTimes函数获得进程的时间使用情况。但是如果想下面的用法就是错误的:获得当前线程的伪句柄,作为父线程句柄参数传递给CreateThread函数,在子线程中对传递下来的伪句柄调用GetThreadTimes。这样子线程获得的是自己的时间,而不是父线程的时间。出现这种状况的原因是线程的伪句柄是当前线程的句柄。网上查了一下伪句柄相关的信息,有一篇blog说GetCurrentProcess和GetCurrentThread始终返回固定的值(-1,-2)。网址如下:http://blog.csdn.net/ouyang2008/archive/2006/08/02/1009760.aspx

通过DuplicateHandle函数可以实现伪句柄到实句柄之间的转化,使用方法如下:

DuplicateHandle(
     GetCurrentProcess(),
     GetCurrentThread(),
     GetCurrentProcess(),
     
& hThreadRealHandle,
     
0 ,
     FALSE,
     DUPLICATE_SAME_ACCESS)

 

你可能感兴趣的:(多线程,数据结构,C++,工作,windows,Signal)