playerken

博客园 首页 新随笔 联系 订阅 管理

线程是由两个部分组成的:

  • 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
  • 另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。

进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地
址空间中执行代码,并且在进程的地址空间中对数据进行操作。

进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。

线程用于描述进程中的运行路径。每当进程被初始化时,系统就要创建一个主线程。该线程与C/C++运行期库的启动代码一道开始运行,启动代码则调用进入点函数(WinMain/main),并且继续运行直到进入点函数返回并且C/C++运行期库的启动代码调用ExitProcess为止。、

通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个GetMessage循环。进程中的所有其他线程都是工作线程,它们与计算机或I/O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。

 

线程函数

每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。线程函数可以执行任何任务。最终,线程函数到达它的结尾处并且返回。这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果使用计数降为0,线程的内核对象就被撤消。与进程内核对象的情况相同,线程内核对象的寿命至少可以达到它们相关联的线程那样长,不过,该对象的寿命可以远远超过线程本身的寿命。

线程函数必须返回一个值,它将成为该线程的退出代码。

 

CreateThread

当CreateThread被调用时,系统创建一个线程内核对象。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。

CreateThread函数是用来创建线程的Windows函数。应该使用编译器供应商自己的CreateThread替代函数(VC中的是_beginthreadex)。

cbStack参数用于设定线程可以将多少地址空间用于它自己的堆栈。当CreateProcess启动一个进程时,它就在内部调用CreateThread来对进程的主线程进行初始化。对于cbStack参数来说,CreateProcess使用存放在可执行文件中的一个值。可以使用链接程序的/STACK开关来控制这个值:/STACK:[reserve][,commit]。
reserve参数用于设定系统应该为线程堆栈保留的地址空间量。默认值是1 MB。Commit参数用于设定开始时应该承诺用于堆栈保留区的物理存储器的容量。默认值是1页。当线程溢出它的堆栈时,就生成一个异常条件。系统抓取该异常条件,并且将commit参数设定的值用于保留空间,这使得线程的堆栈能够根据需要动态地扩大。通过设置一个堆栈限制值,就可以防止应用程序用完大量的物理存储器,同时,也可以更快地知道何时程序中出现了循环递归错误。

Windows是个抢占式多线程系统,这意味着新线程和调用CreateThread的线程可以同时执行。

fdwCreate参数可以设定用于控制创建线程的其他标志。如果该值是0,那么线程创建后可以立即进行调度。如果该值是CREATE_SUSPENDED,系统可以完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。

CreateThread的最后一个参数是dwThreadId,它必须是DWORD的一个有效地址,CreateThread使用这个地址来存放系统分配给新线程的ID。

 

终止线程的运行

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

  • 线程函数返回(最好使用这种方法)。它可以确保:
    • 在线程函数中创建的所有C++对象均将通过它们的撤消函数正确地撤消。
    • 操作系统将正确地释放线程堆栈使用的内存。
    • 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
    • 系统将递减线程内核对象的使用计数。
  • 通过调用ExitThread(_endthreadex)函数,线程将自行撤消(最好不要使用这种方法)。该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。
  • 同一个进程或另一个进程中的线程调用TerminateThread函数(应该避免使用这种方法)。
  • 包含线程的进程终止运行(应该避免使用这种方法)。

当使用返回或调用ExitThread的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。Microsoft故意用这种方法来实现TerminateThread。如果其他仍然正在执行的线程要引用强制撤消的线程堆栈上的值,那么其他的线程就会出现访问违规的问题。如果将已经撤消的线程的堆栈留在内存中,那么其他线程就可以继续很好地运行。

当线程终止运行时,会发生下列操作:

  • 线程拥有的所有用户对象均被释放。在Windows中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。
  • 线程的退出代码从STILL_ACTIVE改为传递给ExitThread或TerminateThread的代码。
  • 线程内核对象的状态变为已通知。
  • 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。
  • 线程内核对象的使用计数递减1。

 

线程的一些性质

调用CreateThread可使系统创建一个线程内核对象。该对象的初始使用计数是2。线程的内核对象的其他属性也被初始化,暂停计数被设置为1,退出代
码始终为STILL_ACTIVE,该对象设置为未通知状态。

一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。然后系统将两个值写入新线程的堆栈的上端。写入堆栈的第一个值是传递给CreateThread的pvParam参数的值。紧靠它的下面是传递给CreateThread的pfnStartAddr参数的值。

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

指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。当线程的内核对象被初始化时,CONTEXT结构的堆栈指针寄存器被设置为线程堆栈上用来放置pfnStartAddr的地址。指令指针寄存器置为称为BaseThreadStart的未文档化的函数的地址中。

当线程完全初始化后,系统就要查看CREATE_SUSPENDED标志是否已经传递给CreateThread。如果该标志没有传递,系统便将线程的暂停计数递减为0,该线程可以调度到一个进程中。然后系统用上次保存在线程上下文中的值加载到实际的CPU寄存器中。这时线程就可以执行代码,并对它的进程的地址空间中的数据进行操作。

 

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

1

标准C运行期库是1970年问世的,它远远早于线程在任何应用程序上的应用。运行期库的发明者没有考虑到将C运行期库用于多线程应用程序的问题。若要使多线程C和C++程序能够正确地运行,必须创建一个数据结构,并将它与使用C/C++运行期库函数的每个线程关联起来。当你调用C/C++运行期库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。

若要创建一个新线程,绝对不要调用操作系统的CreateThread函数,必须调用C/C++运行期库函数_beginthreadex。同样,始终都应该设法避免使用ExitThread函数,而要使用_endthreadex。

C/C++运行期库的函数需要为它创建的每个线程设置单独的数据块。_beginthreadex会分配数据块,再对它进行初始化,将该数据块与你创建的线程联系起来。_endthreadex函数会在线程终止运行时释放数据块。一旦数据块被初始化并且与线程联系起来,线程调用的任何需要单线程实例数据的C/C++运行期库函数都能很容易地检索调用线程的数据块地址,并对线程的数据进行操作。

如果创建一个多线程应用程序,必须在编译器的命令行上设定/MT(指多线程应用程序)或/MD(指多线程DLL)开关。这将使编译器能够定义_MT标识符,定义并调用多线程版本的库函数。多线程版本的C/C++运行期库还给某些函数设置了同步的基本要素。例如,如果两个线程同时调用malloc,那么内存堆栈就可能遭到破坏。多线程版本的C/C++运行期库能够防止两个线程同时从堆栈中分配内存。为此,它要让第二个线程等待,直到第一个线程从malloc返回。

显然,所有这些附加操作都会影响多线程版本的C/C++运行期库的性能。这就是为什么Microsoft公司除了多线程版本外,还提供单线程版本的静态链接的C/C++运行期库的原因。

_endthread函数还存在另一个很难注意到的大问题。在_endthread调用ExitThread之前,它调用CloseHandle,传递新线程的句柄。新的_endthreadex函数并不关闭线程的句柄。

当线程函数返回时,_beginthreadex调用_endthreadex,而_beginthread则调用_endthread。

 

伪句柄

GetCurrentProcess和GetCurrentThread分别返回调用线程的进程伪句柄和线程伪句柄。这些函数并不在创建进程的句柄表中创建新句柄。还有,调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用CloseHandle,将伪句柄作为参数来传递,那么CloseHandle就会忽略该函数的调用并返回FALSE。

线程的伪句柄是当前线程的句柄。DuplicateHandle可以将伪句柄转换成实句柄。由于DuplicateHandle会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给CloseHandle,从而递减对象的使用计数。

posted on 2011-08-28 18:05  playerken  阅读(283)  评论(0编辑  收藏  举报