(3) 正确编写C/C++运行库下的线程

最后更新于:2022-04-01 21:44:47

## 如何在C/C++运行库下正确的启用新线程 由于C/C++的历史早于线程的出现,因此C/C++的函数并不都是线程安全的。如全局变量errno等。 这就需要一种解决方案。一种方法是利用属于每个线程的数据块,该数据块不会被线程共享,而只能够用于线程自己,这样类似errno的情况便迎刃而解。 此外,C/C++运行库针对特定函数做了改写,使其能够进行线程同步。如malloc函数,由于不能够多线程同时执行内存堆分配操作,因此多线程版本的运行库进行了线程同步处理。 那么,如何让windows系统知道当我们创造新线程时,为我们分配属于线程的存储区呢?利用CreateThread函数并不行(C/C++运行库若获取不到存储器,会自动请求分配对应存储区,因此CreateThread函数实际也可以支持线程安全,但还有其他问题下面再说),因为他只是一个系统API,他不会知道你所写的是C\C++代码。 ## _beginthreadex函数 _beginthreadex是C/C++运行库创建线程函数,因此可以完美支持C/C++代码的线程安全。其声明如下: ~~~ uintptr_t _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr ); ~~~ 其参数意义与CreateThread函数完全相同。 重点是要理解该函数为C/C++线程安全做了那些事情。我们可以看到其函数定义。(VS2012路径为C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\crt\src\threadex.c) ~~~ _CRTIMP uintptr_t __cdecl _beginthreadex ( void *security, unsigned stacksize, unsigned (__stdcall * initialcode) (void *), void * argument, unsigned createflag, unsigned *thrdaddr ) { _ptiddata ptd; /* pointer to per-thread data */ uintptr_t thdl; /* thread handle */ unsigned long err = 0L; /* Return from GetLastError() */ unsigned dummyid; /* dummy returned thread ID */ /* validation section */ _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0); /* * Allocate and initialize a per-thread data structure for the to- * be-created thread. */ if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL ) goto error_return; /* * Initialize the per-thread data */ _initptd(ptd, _getptd()->ptlocinfo); ptd->_initaddr = (void *) initialcode; ptd->_initarg = argument; ptd->_thandle = (uintptr_t)(-1); #if defined (_M_CEE) || defined (MRTDLL) if(!_getdomain(&(ptd->__initDomain))) { goto error_return; } #endif /* defined (_M_CEE) || defined (MRTDLL) */ /* * Make sure non-NULL thrdaddr is passed to CreateThread */ if ( thrdaddr == NULL ) thrdaddr = &dummyid; /* * Create the new thread using the parameters supplied by the caller. */ if ( (thdl = (uintptr_t) CreateThread( (LPSECURITY_ATTRIBUTES)security, stacksize, _threadstartex, (LPVOID)ptd, createflag, (LPDWORD)thrdaddr)) == (uintptr_t)0 ) { err = GetLastError(); goto error_return; } /* * Good return */ return(thdl); /* * Error return */ error_return: /* * Either ptd is NULL, or it points to the no-longer-necessary block * calloc-ed for the _tiddata struct which should now be freed up. */ _free_crt(ptd); /* * Map the error, if necessary. * * Note: this routine returns 0 for failure, just like the Win32 * API CreateThread, but _beginthread() returns -1 for failure. */ if ( err != 0L ) _dosmaperr(err); return( (uintptr_t)0 ); } ~~~ **可以看到_beginthreadex函数做了以下事项:** 1、在函数开始处,在C/C++运行库堆上分配并初始化每个线程的私有内存ptd。 2、我们初始传入的线程函数与线程参数被存储到ptd中。 3、_beginthreade最终调用CreateThread函数运行线程(毕竟windows系统只认识其API)。 4、注意在CreateThread函数中,线程函数替换为另一函数_threadstartex,同时线程参数传入了ptd。 ## _threadstartex函数 由_beginthreadex函数定义可以知道,我们的线程函数,其实首先执行的都是_threadstartex。那么我们看看该函数都做了什么。 ~~~ static unsigned long WINAPI _threadstartex ( void * ptd ) { _ptiddata _ptd; /* pointer to per-thread data */ /* * Check if ptd is initialised during THREAD_ATTACH call to dll mains */ if ( ( _ptd = (_ptiddata)__crtFlsGetValue(__get_flsindex())) == NULL) { /* * Stash the pointer to the per-thread data stucture in TLS */ if ( !__crtFlsSetValue(__get_flsindex(), ptd) ) ExitThread(GetLastError()); /* * Set the thread ID field -- parent thread cannot set it after * CreateThread() returns since the child thread might have run * to completion and already freed its per-thread data block! */ ((_ptiddata) ptd)->_tid = GetCurrentThreadId(); _ptd = ptd; } else { _ptd->_initaddr = ((_ptiddata) ptd)->_initaddr; _ptd->_initarg = ((_ptiddata) ptd)->_initarg; _ptd->_thandle = ((_ptiddata) ptd)->_thandle; #if defined (_M_CEE) || defined (MRTDLL) _ptd->__initDomain=((_ptiddata) ptd)->__initDomain; #endif /* defined (_M_CEE) || defined (MRTDLL) */ _freefls(ptd); ptd = _ptd; } #if defined (_M_CEE) || defined (MRTDLL) DWORD domain=0; if(!_getdomain(&domain)) { ExitThread(0); } if(domain!=_ptd->__initDomain) { /* need to transition to caller's domain and startup there*/ ::msclr::call_in_appdomain(_ptd->__initDomain, _callthreadstartex); return 0L; } #endif /* defined (_M_CEE) || defined (MRTDLL) */ _ptd->_initapartment = __crtIsPackagedApp(); if (_ptd->_initapartment) { _ptd->_initapartment = _initMTAoncurrentthread(); } _callthreadstartex(); /* * Never executed! */ return(0L); } ~~~ **上面代码很多,大体看下就好。要了解的是:** 1、和往常一样,CreateThread后,系统会先调用RtlUserThreadStart,然后由其调用_threadstartex。 2、在_threadstartex中,调用了系统API [TlsSetValue](https://msdn.microsoft.com/en-us/library/aa908728.aspx) 来讲ptd与调用线程关联起来(TLS 线程本地存储)。 3、_threadstartex调用  _callthreadstartex() 来运行我们最初传入的线程函数。 ##  _callthreadstartex函数 经历了上面种种,最终我们传入的线程函数,会被 _callthreadstartex函数调用。其定义如下: ~~~ static void _callthreadstartex(void) { _ptiddata ptd; /* pointer to thread's _tiddata struct */ /* must always exist at this point */ ptd = _getptd(); /* * Guard call to user code with a _try - _except statement to * implement runtime errors and signal support */ __try { _endthreadex ( ( (unsigned (__CLR_OR_STD_CALL *)(void *))(((_ptiddata)ptd)->_initaddr) ) ( ((_ptiddata)ptd)->_initarg ) ) ; } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { /* * Should never reach here */ _exit( GetExceptionCode() ); } /* end of _try - _except */ } ~~~ 该函数很简单,就是拿出ptd的值,同时,调用了_endthreadex函数来执行并返回我们的线程函数。  ## _endthreadex函数 与_beginthreadex函数对应,_endthreadex是C/C++运行库终止线程运行的函数,其定义如下 ~~~ /*** *_endthreadex() - Terminate the calling thread * *Purpose: * *Entry: * Thread exit code * *Exit: * Never returns! * *Exceptions: * *****************************************/ void __cdecl _endthreadex ( unsigned retcode ) { _ptiddata ptd; /* pointer to thread's _tiddata struct */ ptd = _getptd_noexit(); if (ptd) { if (ptd->_initapartment) _uninitMTAoncurrentthread(); /* * Free up the _tiddata structure & its subordinate buffers * _freeptd() will also clear the value for this thread * of the FLS variable __flsindex. */ _freeptd(ptd); } /* * Terminate the thread */ ExitThread(retcode); } ~~~ **与_beginthreadex函数对应,** 1、_endthreadex销毁了在_beginthreadex分配的堆内存(保证了没有内存泄露)。 2、其调用了系统API ExitThread退出线程。 ## ExitThread  VS _endthreadex 在编写C\C++程序时,要调用_endthreadex来结束线程。基于如下两个理由: 1、ExitThread函数非C++函数,线程创建的C++对象不会得到析构。 2、若线程中使用了ptd,ExitThread不会释放内存,造成内存泄露。 ## CreateThread VS _beginthreadex 一般的理由是,CreateThread有可能照成内存泄露。(如果使用了ptd内存,而CreateThread并不会在内部自动调用释放内存函数,但若链接的是C/C++运行库的dll版本,则其会在线程退出的DLL_THREAD_DETCH通知中释放内存)。 ## 不要调用的C/C++函数 _beginthreadex和_endthreadex分别有两个比较老的版本: ~~~ uintptr_t _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist ); void _endthread( void ); ~~~ 我们应该忘记这两个函数,不要调用它们。 对于_beginthread函数,可以看出其函数参数是较少的,例如其中不包括安全属性,让我们对线程的控制力没有其增强版本多。 同时,由于在_beginthread内部会调用_endthread函数,而该函数多此一举的会调用一次CloseHandle,来帮我们关闭线程句柄。似乎有友好的操作,但实际会照成问题。例如下边代码 ~~~ HANDLE hThread = _beginthread(...); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); ~~~ 在真正调用WaitForSingleObject之前,_beginthread函数里的线程可能已经执行完毕,系统会释放一次线程handle句柄,同时,_endthread又会释放一次handle句柄(此时handle计数为0)。那么再调用WaitForSingleObject时,可能这时的hThread已经是一个无效句柄,导致函数调用失败,同理,对CloseHandle也是一样。
';