看了MoreWindows同学的文章《秒杀多线程第二篇 多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》,顺便又翻了一下《windows核心编程》,发现以前看过一遍的东西都忘记了。为了加深印象,所以把一些重要点记录下来。
以下内容读者都可以在《windows核心编程》第6章 线程基础中找到。
1 CreateThread不会在创建线程之前申请一个_tiddata结构内存块,当一个线程调用一个需要_tiddata结构的C/C++运行库函数(例如errno, strtok, _gmtime等)时,虽然C/C++运行库函数会为主调函数申请并初始化一个_tiddata块,但是在线程退出的时候,如果不是调用_endthreadex函数的话,_tiddata块将不能被释放,从而导致内存泄露。(这里会有一个例外,后面说)
2 如果线程使用了C/C++运行库的signal函数(该函数用于设置中断信号处理函数,比如可以使用此函数屏蔽CTRL+C,更多介绍可参阅MSDN),则整个进程会异常退出。因为CreateThread创建的线程其结构化异常处理帧(SEH)没有设置,导致程序不能处理异常。
1 对照前面第一点中提到的CreateThread的隐患,_beginthreadex都做了相应的处理,首先_beginthreadex函数会申请_tiddata数据结构块,然后会将要创建线程的入口点和参数都设置到该_tiddata数据结构中。
2 _beginthreadex内部确实调用了CreateThread这一系统API,因为这是操作系统创建新线程的唯一方法。
但是调用CreateThread时,所给的新线程入口点是_threadstartex,而不是外部由程序员提供的线程入口地址,同时,传递给CreateThread的参数也不是外部由程序员提供的参数,而是_threadstartex申请的_tiddata数据结构块。
所以,接下来的工作都将转入到_threadstartex函数中。
1 首先使用TlsSetVlue函数将_tiddata结构和线程关联起来。
2 初始化浮点操作支持。
3 设置SEH链,捕捉C++异常,例如在第一点中提到的C/C++运行库的signal函数,需要异常处理支持。
4 以上工作就绪后,会调用保存在_tiddata中由外部程序员提供的线程入口函数,并传入保存在_tiddata中的参数,这样就启动了线程代码。
5 该函数将一直等待线程的结束,当线程返回时,调用_endthreadex函数,在_endthreadex内部通过_getptd和_freeptd释放掉_tiddata结构的内存。_endthreadex函数最终会调用系统的ExitThread API(这时候调用该函数已经是安全的了,因为_tiddata内存已经释放)。
这一点在第三点中已经说明了,直接调用ExitThread 函数会导致_tiddata内存无法释放,如果不得已,外部必须要结束一个线程,可以调用_endthreadex来结束线程,_endthreadex会释放_tiddata内存块。
具体原因参阅《windows核心编程》。
根据《windows核心编程》中的说明,当模块链接到C/C++运行库的DLL版本时,这个库(例如VS2008的msvcr90d.dll,msvcr90.dll)会在线程终止时收到一个DLL_THREAD_DETACH通知,然后会执行释放_tiddata块(如果有申请)。虽然这是可以防止_tiddata块内存泄露的,但仍然强烈建议使用_beginthreadex来创建线程,而不要使用CreateThread。因为不能保证所有使用者都使用动态链接库版本。
对于msvcr90d.dll在线程销毁时所做的工作,我用反汇编工具IDA加载看了一下,关键代码如下:
text:1023C889 loc_1023C889: ; CODE XREF: sub_1023C610+1CAj .text:1023C889 cmp [ebp+arg_4], 3 ; 这里比较是不是DLL_THREAD_DETACH .text:1023C88D jnz short loc_1023C899 .text:1023C88F push 0 .text:1023C891 call __freeptd ; 释放_tiddata结构,及结构中申请的内存 .text:1023C896 add esp, 4
__freeptd函数中调用了 _freefls函数,该函数会释放_tiddata内存块。
==================全文完==================