VC中创建线程分析

VC中创建线程分析

1、CreateThread、_beginthreadex、AfxBeginThread的区别和正确使用:

CreateThread是一个Windows 的API函数,_beginthreadex是一个微软VC中C运行时库中的线程创建函数,AfxBeginThread则是MFC中的线程创建函数。

其依赖关系为:<--表示被依赖

CreateThread <--_beginthreadex

CreateThread <-- AfxBeginThread

_beginthreadex为每个使用线程在Heap上创建(用__calloc_crt,相当于calloc)了一个tiddata结构并且设定到动态TLS。这样C运行时库中使用静态变量的几个函数就可以得到只和线程相关的一份“静态”和“全局”变量了,C运行时全局变量也不会互相干扰。假若使用CreateThread创建了这同样的线程,因为没有事先分配这个tiddata结构,那么后果自然就很严重了。

但是使用了_beginthreadex也有一个不算麻烦的小麻烦。

线程最好的退出方式是从return退出,所有的资源都会正确的释放,如果调用ExitThread退出线程,那么线程堆栈上的内容被自动清除,但是C++对象不能够通过调用析构函数而正确的清除。而和_beginthreadex对应的_endthreadex恰恰就是调用了ExitThread来终止线程。所以,这下_endthreadex也不能调用了。那么在前面分配的tiddata怎么办?没有任何办法,除非去调用_endthreadex才会得到释放,但是ExitThread语句又紧跟在后边。还好,tiddata结构非常小,不超过100个字节,但是要是遇上傻大黑粗的野蛮程序员,在进程生命期中反复创建线程,也挺恐怖。

所以最好的做法就是,在线程中,避免使用C运行时函数,用C++运行时库或者Windows API解决问题。这样就可以避免使用_beginthreadex,而用CreateThread创建线程,也不会有tiddata结构的运行时内存泄漏了。(或者,不在线程中使用C++对象,这样就不怕_endthreadex的ExitThread 了。  馊主意!不过_beginthreadex本来就是为使用C函数的线程准备的。)

至于AfxBeginThread,是MFC中定义的一个函数,如果你使用MFC框架,那么应该使用这个函数,这样更符合框架的设计目的。这个函数内部的实现就是通过CreateThread,因为既然都使用MFC了,还费劲使用C函数库干嘛?

2、CreateThread的不知道算不算Bug的Bug:

CreateThread的定义请参见不同版本的MSDN。

这个地址是VS2008 MSDN中的定义 http://msdn.microsoft.com/en-us/library/ms682453(VS.85).aspx

该API函数的第2个参数 dwStackSize 以及 第5个参数 dwCreationFlag 分别指定了线程堆栈的大小和创建标志。

按照文档中的说明,dwCreationFlag有一个设定值为STACK_SIZE_PARAM_IS_A_RESERVATION。这个标志的含义是,将指定大小的堆栈在虚拟内存中分配,并且初始只映射系统缺省页数的物理内存(大概是两个页),随着堆栈使用的增大,系统根据堆栈物理内存页后方的设定为PAGE_GUARD属性的页触发来把更多的物理内存映射到后续的页。

值得注意的是,STACK_SIZE_PARAM_IS_A_RESERVATION这个标志只在Windows XP以及Windows 2003以及更新的系统上才会得到支持。如果没有设定这个标志,并且也让线程一创建就开始运行(大部分时候是这样),那么系统会参照dwStackSize中指定的堆栈大小,并参照内存分配最小粒度以及页对齐原则,把一定大小的物理内存映射给堆栈。

看上去一切都很不错,但是如果你尝试创建一个会很快耗光堆栈的线程ThreadProc,并且用下面的语句创建:

CreateThread(NULL, 1024 * 1024 /* 1MB */, ThreadProc, NULL, 0, &threadId);

然后在线程中精心设置好异常捕捉,满心欢喜的等待EXCEPTION_STACK_OVERFLOW的到来。

那么你会震惊的发现:

程序因为EXCEPTION_ACCESS_VIOLATION异常而直接退出了。没有EXCEPTION_STACK_OVERFLOW。

你重新设置异常条件,决定捕捉全部的异常,但是你会发现你完全无法捕捉。Why?

接下来,你把堆栈大小设置为:1024 * 1024 + 1 或者1024 * 1024 - 8092,那么又可以准确的捕捉到溢出异常了。

或者你把dwStackSize设置为0,或者给dwCreationFlag 加上STACK_SIZE_PARAM_IS_A_RESERVATION标志,也可以成功捕捉异常。

现在应该知道原因了。运行在x86上的Windows XP,目前内存分配最小粒度为64KB,用户态下页面大小是4KB。

当把dwStackSize设置为0时,系统会缺省设置堆栈为1MB大小,但是事实上线程得不到1MB的堆栈,因为系统会在堆栈最后的部分用加上PAGE_GUARD属性的页来检测溢出,并且还要保留用于处理溢出异常发生时的页面,所以可以检测到异常。

而给dwCreationFlag 加上STACK_SIZE_PARAM_IS_A_RESERVATION标志时,按照64KB分配粒度的原则,堆栈最后的部分始终有足够的页来检测溢出和处理异常。所以也很安全。

而为线程指定1024 * 1024大小堆栈时,堆栈边界正好和64KB以及1MB边界对齐的,当完全提交物理内存时,堆栈的所有页的属性都将是PAGE_READWRITE(事实上应该是一个Windows的Bug,但是考虑性能原因而故意让其存在)。所以系统根本没机会检测到溢出错误,而会在堆栈溢出后直接引发EXCEPTION_ACCESS_VIOLATION。而因为没有预留的异常处理页面,异常处理程序又会引发一个EXCEPTION_ACCESS_VIOLATION异常。天啦!!整个进程就这样崩溃了。

而为什么指定1024 * 1024 + 1 或者1024 * 1024 - 8092就可以捕捉异常了呢?因为堆栈大小在超过1MB后,是按照1MB对齐原则,哪怕多一个字节,系统也会多映射1MB的物理内存。这样足够两页的异常检测和异常处理页面。1024 * 1024 - 8092大小正是这个原因所以能正确引发异常。

所以,正确的做法是,如果想在运行CreateThread时指定堆栈大小,并且全部提交物理内存的话,那么大小必须保证在整1MB边界上留出至少两个页面给系统。

你可能感兴趣的:(VC中创建线程分析)