线程创建与C/C++运行时库函数的笔记

来源:互联网 发布:淘宝普拉松自动饮水器 编辑:程序博客网 时间:2024/04/28 22:07

搞不懂_beginthreadex()到底啥好处,下面这边文章介绍的挺清楚的。

http://blog.csdn.net/bigric3/article/details/21949485



在《计算机病毒揭秘与对抗》一书中看到的,感觉很不错,这里精其核心做一篇笔记。


C/C++运行时库函数


CRT(C/C++ RunTime Library)是支持C/C++运行的一系列函数和代码的总称。C/C++程序中用户代码的入口函数main或WinMain即其负责调用,诸如strlen、strtok(字符串分割)、time、atoi、rand之类的函数也是由其提供。

[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. char  *strtok ( char *strToken, const char *strDelimit ) ;  

strtok函数在第二次调用时如未传入新的源字符串,则接着上一次查找到的子串后面继续查找新的子串。要实现这个功能,strtok函数内部必须使用一个全局变量或者静态变量在每次调用结束前保存本次函数查找到的子串后面的字符串。这样当下次调用时,如果传入的源字符串是null,则把上次调用被赋值的全局或者静态变量作为源字符串,从而实现这样的功能。

可以想象对于一个多线程程序,上述的这种机制肯定是会出问题的。出问题的根本原因是所有线程都共享同一个静态变量,那么在一个线程中改变了变量的值将会影响其他的线程。解决这个问题就要使用线程内全局变量。其含义即该全局变量在线程内所有函数中都可见,但是在不同线程中不共享从而不会相互影响。


线程内全局变量的实现


实现线程内全局的方法称为TLS( Thread Local Storage )。TLS的实现过程需要4个函数:

  • TlsAlloc            /* 在TLS数组中找到一个没有用过的数组位置 */
  • TlsSetValue    /* 将需要保存的值存储到TlsAlloc函数找到的数组位置 */
  • TlsGetValue    /* 即可获得先前保存的值 */
  • TlsFree            /* 使用完毕后调用TlsFree重新把已使用完毕的那个数组位置重新设置为未使用状态,以便下次TlsAlloc可以继续使用此位置 */
每个线程创建时系统给其分配一个LPVOID的数组(TLS数组),这个数组从C语言的角度说是隐藏着不能直接访问的。需要通过上述TLS函数调用访问。这些数组即保存线程内全局变量的值。

∵有许多C/C++运行时库函数都涉及在多线程中的调用问题,都需要使用TLS技术保存一个线程内的全局变量。∴C/C++运行时库设计了一个公共结构体_tiddata,这个结构体保存了类似strtok、rand函数所需要的线程内全局变量。

在strtok或rand函数中,如果用于多线程工程中,那么这些函数中总有这样的一行代码:_piddata  ptd = _getptd() ; _getptd内部实际上就是调用TlsGetValue函数把本线程_tiddata结构体的首地址取出来,这个结构体的相应成员即保存了strtok或rand等函数上一次调用时需要保存的值,下面的问题是_getptd函数获得的_tiddata结构体是何时分配内存并把其地址保存到TLS中的呢?

对于使用CRT的程序,在调用用户入口函数(main或WinMain)前CRT都会调用TlsAlloc函数获得一个未使用的TLS数组位置,然后将在内存堆区分配一个_tiddata结构体空间,并把这块内存的首地址使用TlsSetValue存放到主线程TLS中。∴主线程中调用这些CRT函数也不会出错。

下一个问题:CRT是如何处理单/多线程,是否使用TLS技术的呢?为保证多线程不出问题,是不是无论是否为多线程工程都是用TLS技术?
答案是否定的,CRT根据工程设置,当工程设置为多线程工程时(VC6下:Project --> Settings --> C/C++ 这个属性页中的Category一项选择:Code Generation,在Use run time library 选择Multithreaded即可),才使用TLS技术,否则对于VC6下应该是使用静态变量。当我们把工程设置为多线程工程那么系统将定义一个_MT宏,相反单线程则不存在。对于strtok、rand之类的CRT函数即是通过判断_MT宏是否存在,而决定是否使用TLS技术。按照原文中所说,strtok.c看到是否是多线程工程的判断如下:
[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. #ifdef  _MT  
  2.       _ptiddata  ptd  =  _getptd() ;  
  3. #else  /* _MT */  
  4.       static  char  * nextoken ;  
  5. #endif  /* _MT */  

实际上我在读VS2012中的strtok.c中并不是这样的,也没有上述代码,而是:
[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. #ifdef _SECURE_VERSION  
  2.   
  3.         /* validation section */  
  4.         _VALIDATE_RETURN(context != NULL, EINVAL, NULL);  
  5.         _VALIDATE_RETURN(string != NULL || *context != NULL, EINVAL, NULL);  
  6.         _VALIDATE_RETURN(control != NULL, EINVAL, NULL);  
  7.   
  8.         /* no static storage is needed for the secure version */  
  9.   
  10. #else  /* _SECURE_VERSION */  
  11.   
  12.         _ptiddata ptd = _getptd();  
  13.   
  14. #endif  /* _SECURE_VERSION */  

应该是微软取消了在strtok中继续使用静态变量的方式,无论是否为多线程程序,都会使用TLS技术,有区别在于当前工程是否定义了使用安全版函数,即strtok与strtok_s的区别,安全版的strtok是不需使用TLS技术的,而是使用一个const的二级指针保存每次操作后的子字符串。


下面继续看多线程中TLS的问题

在多线程中只要在创建线程时为这个线程分配一个属于他的_tiddata结构体,并把这个结构体首地址保存到该线程的TLS中,这样CRT的一些库函数调用_getptd就可获得属于自己线程的_tiddata结构体。如果我们在主线程中调用CreateThread创建一个线程,并调用strtok之类的CRT函数,新线程中并没有申请_tiddata结构体所以其TLS也没有保存这样的结构体地址,因此_getptd就会获得一个NULL,那么strtok函数即会出现问题。CRT为保证不出现这个问题使用如下方法:
[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. _ptiddata __cdecl _getptd (  
  2.         void  
  3.         )  
  4. {  
  5.         _ptiddata ptd = _getptd_noexit();  
  6.         if (!ptd) {  
  7.             _amsg_exit(_RT_THREAD); /* write message and die */  
  8.         }  
  9.         return ptd;  
  10. }  

[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. _ptiddata __cdecl _getptd_noexit (  
  2.         void  
  3.         )  
  4. {  
  5.     _ptiddata ptd;  
  6.     DWORD   TL_LastError;  
  7.   
  8.     TL_LastError = GetLastError();  
  9.   
  10.   
  11.     if ( (ptd = __crtFlsGetValue(__flsindex)) == NULL ) {  
  12.         /* 
  13.          * no per-thread data structure for this thread. try to create 
  14.          * one. 
  15.          */  
  16. #ifdef _DEBUG  
  17.         extern void * __cdecl _calloc_dbg_impl(size_tsize_tintconst char *, intint *);  
  18.         if ((ptd = _calloc_dbg_impl(1, sizeof(struct _tiddata), _CRT_BLOCK, __FILE__, __LINE__, NULL)) != NULL) {  
  19. #else  /* _DEBUG */  
  20.         if ((ptd = _calloc_crt(1, sizeof(struct _tiddata))) != NULL) {  
  21. #endif  /* _DEBUG */  
  22.   
  23.             if (__crtFlsSetValue(__flsindex, (LPVOID)ptd) ) {  
......
return(ptd);

__crtFlsSetValue,该函数是操作系统函数,即所谓的线程局部存储(Thread Local Storage, TLS)。
通过上面的代码可以看出当去获取线程内先前保存的_tiddata结构体地址失败后,会在堆区申请一个结构体,然后把这个结构体保存到本线程的TLS中,最后返回自己创建的这个_tiddata结构体首地址。
这样一些需要TLS支持的CRT函数就一定可以拿到_tiddata结构体,从而保证多线程下不会出问题,但这里还有一个严重的问题:若新线程中没有_tiddata结构体,那么_getptd会从堆区中申请一个返回,那么这块内存由谁来释放呢?


些需TLS支持的CRT函数在多线程中引起的内存泄漏问题


使用CreateThread函数创建的线程中,使用需TLS支持的CRT函数,使用完毕后并没有释放第一次调用时在堆区分配的_tiddata结构体,这样导致了内存泄漏。

如何解决上述问题呢?

使用_beginthread / _beginthreadex 函数创建线程。以beginthread为例( beginthreadex同理 ),beginthread内部也是调用CreateThread创建线程,只不过在创建新线程前在堆区分配一个_tiddata结构体,并把用户要执行的线程函数地址保存于_tiddata结构体的_initaddr成员,然后把要传递给线程函数的参数保存于_tiddata结构体的_initarg成员中。之后调用CreateThread创建线程,然而创建的线程入口函数不是用户传入的线程函数的地址,而是一个CRT事先准备好的名为_threadstart的函数,原型如下:
[cpp] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. static   unsigned   long   WINAPI   _threadstart ( void * ) ;  

同时把刚刚在堆区创建的_tiddata结构体首地址作为线程函数参数传进去。这样新线程开始运行的是_threadstart函数的代码。在这个线程函数中首先调用TlsSetValue将传进来的_tiddata首地址保存至本线程的TLS,然后通过_tiddata结构体中的_initaddr成员调用用户真正的线程函数。那这块结构体内存由谁来释放呢?

由_threadstart进行释放,当他调用完毕_tiddata结构体的_initaddr成员所保存的用户线程后,也就意味着用户线程函数已经调用结束,最后将调用一个_endthread函数,这个函数首先使用CloseHandle将_tiddata结构体的_thandle成员所保存的本线程的句柄关闭,然后调用_freeptd(_ptiddata ptd)将先前在堆区申请的_tiddata结构内存释放,这样就不会造成内存泄漏。

同样我们在调用CreateThread函数创建线程,在线程函数尾显示调用一下_endthread函数也可以释放内存,避免内存泄漏。

0 0