createthread用法,_beginthread()

createthread用法,_beginthread(),CreateThread()与beginthread()的区别详细解析

很多开发者不知道两者的关系,随意选择一个函数使用,发现问题不大,就忙着解决更紧急的任务。有一天,突然发现一个程序运行久了会有轻微的内存泄漏。开发者绝不会认为是这两套功能混合的结果。

我们知道在Windows下创建线程有两种方法,一种是调用Windows API CreateThread()创建线程;另一种是调用MSVC CRT的函数_beginthread()或_beginthreadex()创建线程。对应的ExitThread也有两个函数:Windows API的ExitThread()和CRT的_endthread()。这两组函数用于创建和退出线程。两者有什么区别?

很多开发者不知道两者的关系,随意选择一个函数使用,发现问题不大,就忙着解决更紧急的任务,而不是去钻研。当有一天,突然发现一个程序运行很久,会有轻微的内存泄漏。开发者绝不会认为是这两套功能混合的结果。

根据Windows API和MSVC CRT的关系,可以看出_beginthread()是CreateThread()的包装器,最终调用CreateThread()创建线程。那么在_beginthread()调用CreateThread()之前做了什么呢?我们可以看一下_beginthread()的源代码,它位于CRT源代码中的thread.c。我们可以发现它在调用CreateThread()之前申请了一个名为_tiddata的结构,然后用_initptd()函数初始化这个结构,并传递给_beginthread()自己的线程入口函数_threadstart。_threadstart首先将_beginthread()传递的_tiddata结构的指针保存到线程的显式TLS数组中,然后它调用用户的线程入口来真正启动线程。用户线程结束后,_threadstart()函数调用_endthread()结束线程。而_threadstart还用__try/__except包装了用户线程入口函数,用来捕获所有未处理的信号,交给CRT处理。

所以除了信号,很明显CRT包装Windows API线程接口的主要目的就是那个_tiddata。这个线程的私有结构中存储了什么?我们可以从mtdll.h中找到它的定义,里面保存了与CRT相关的信息,比如线程ID、线程句柄、erron、strtok()的最后一次调用位置、rand()函数的种子、异常处理以及其他线程私有的信息。可以看出,MSVC CRT并没有使用我们前面提到的__declspec(thread)方法来定义线程私有变量,以防止库函数在多线程下失效。而是在堆上申请了一个_tiddata结构,把线程私有变量放在结构里面,用显式TLS保存_tiddata的指针。

知道了这些信息,我们就要想到一个问题,那就是如果我们用CreateThread()创建一个线程,然后调用CRT的strtok()函数,按理说会出现错误,因为strtok()需要的_tiddata是不存在的,但是我们好像从来没有遇到过这样的问题。再看strtok()函数,你会发现当_getptd()最初被调用来获取线程的_tiddata结构时,如果这个函数发现线程没有申请_tiddata结构,它就会申请这个结构并负责初始化。所以无论我们调用哪个函数来创建线程,都可以放心地调用所有需要_tiddata的函数,因为一旦这个结构不存在,就会被创建。

那么_tiddata什么时候发布呢?ExitThread()肯定不会,因为它根本不知道有_tiddata这样的结构,所以很明显是由_endthread()释放的,这正是CRT所做的。但是我们经常发现,即使使用CreateThread()和ExitThread()(不调用ExitThread()直接退出线程函数的效果是一样的),也不会发现内存泄漏。为什么?经过仔细检查,我们发现原来的密码在CRT DLL的入口函数DllMain中。我们知道,当一个进程/线程启动或退出时,每个DLL的DllMain都会被调用一次,所以CRT的动态链接版本有机会释放DllMain中线程的_tiddata。但是,DllMain仅在CRT是动态链接版本时才起作用。静态链接CRT没有DllMain!这是在使用CreateThread()时导致内存泄漏的情况。在这种情况下,_tiddata无法在线程结束时释放,从而导致泄漏。

我们可以用下面这个小程序来测试:复制代码如下:# include windows . h # include process . h void thread(void * a){ char * r=strtok(' AAA ',' B ');exit thread(0);//这个函数调用与否无关紧要} int main (int argc,char * argv[]){ while(1){ createthread(0,0,(lpthread _ start _ routine) thread,0,0,0);睡眠(5);}返回0;}如果使用动态链接的CRT (/MD,/MDd),就不会有问题。但是,如果你用的是静态链接的CRT (/MT,/MTd),运行程序后,你会发现内存使用量一直在上升,但是如果我们把Thread()函数中的ExitThread()改为_endthread(),就不会有问题,因为_endthread

这个问题可以总结为:使用CRT时(基本上所有程序都使用CRT),请尽量使用这组函数_ beginthread()/_ beginthreadex()/_ endthread()/_ endthreadex()来创建线程。在MFC中,有一组类似的函数,AfxBeginThread()和AfxEndThread()。根据上述原理,它们是MFC级别的线程包装器函数,它们将维护线程的MFC相关结构。我们在使用MFC类库的时候,要尽量使用它提供的线程包装函数,保证程序正确运行。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: