windows多线程基础(5):使用_beginthreadex创建线程(C/C++ Runtime Library考虑)
来源:互联网 发布:网络舆论是什么意思 编辑:程序博客网 时间:2024/06/03 21:11
标准CRT(C Runtime Library)问世于1970年,没有考虑到将CRT运行于多线程应用程序的问题。
- 在多线程环境中存在问题的C/C++运行期库变量和函数包括:error, _doserrno, strtok, strerror, asctime, gmtime,malloc等等。这些函数中都存在全局变量或者要访问全局变量(对于malloc来说,进程的堆也算是全全局的了),所以导致多个线程同时访问时,会发生互相影响。这下就会乱套了。
俗话说办法总比问题多,好在知道了问题之后,就会有解法了。
- 若要使多线程C/C++程序正确运行,必须创建一个数据结构,并将它与使用C/C++运行期库函数的每个线程关联起来。当调用这些库函数时,这些函数必须知道查看属于线程自己的数据块,这样就不会对别的线程产生影响。
既然系统不帮我们干,那么我们就自己干。(是系统不能干么?当然也不是,或许系统是为了让系统内核的接口保持简单而已,不要加那么多的逻辑在里面。在我们的工作当中不是也是么?比如一个特殊的数据格式转换,是在你的模块里做,还是在我的模块里面做,都可以,不过谁做了,谁的逻辑就稍微麻烦些了。)若要创建一个新线程,不要使用操作系统的CreateThread函数,必须调用C/C++运行期库函数_beginthreadex。
unsigned long _beginthreadex( void *security, unsinged stack_size, unsigned (*start_address)(void *), void *arglist, unsinged initflag, unsigned *thrdaddr_);
- _beginthreadex函数的参数列表与CreateThread函数的参数列表是相同的,但是参数名和类型并不完全相同,据说是VC的C/C++运行期库的开发者认为不应该对Windows的数据类型有任何依赖。同样,返回参数为新创建线程的句柄。
- 我们知道VC的运行库分为单线程静态链接库、多线程静态链接库、多线程动态链接库(我在VC2013里面草草秒了一眼,貌似没有单线程静态链接库了,但是静态多线程连接库貌似有好几个版本了)。_beginthreadex函数只存在多线程的版本中,在使用多线程编译的时候,VC编译器会加上/MT参数。
- VC的运行库提供了源码(位置在Microsoft Visual Studio 12.0\VC\crt\src\threadex.c,我了个去,可以学习一下了同志们),一下是_beginthreadex的源代码:
/* * CreateThread 包装 */static HANDLE _createThread( LPSECURITY_ATTRIBUTES security, unsigned stacksize, LPVOID ptd, unsigned createflag , LPDWORD thrdaddr){ return CreateThread( security, stacksize, _threadstartex, ptd, createflag , thrdaddr);}_CRTIMP uintptr_t __cdecl _beginthreadex ( void * security, unsigned stacksize, unsigned ( __stdcall * initialcode ) (void *), void * argument, unsigned createflag , unsigned * thrdaddr ){ //这个ptd就是为每个线程分配数据块的指针*/ _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,线程ID */ // 检查initialcode非空 /* validation section */ _VALIDATE_RETURN( initialcode != NULL , EINVAL , 0); /* 申请一个ptd数据结构体,这个数据块对于将要创建线程来说是独有的 * 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; /* 初始化ptd结构体的参数(线程函数地址,传递给线程函数的参数,一个句柄,应该是线程句柄吧) * 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) */ /* 确保传递给CreateThread的参数thrdaddr非空 * 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, (LPVOID)ptd, createflag, (LPDWORD)thrdaddr)) == ( uintptr_t)0 ) { err = GetLastError(); goto error_return; } /* 返回 线程句柄 * Good return */ return(thdl); /* * Error return */// 错误返回error_return: /* ptd可能是NULL,或者指向一个应该释放掉的,不再需要的_tiddata结构体数据 * 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); /* 设置校验错误代码(就是用GetLastError()可以获得的代码) * 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 ); // NULL无效函数句柄}
关于_beginthreadex的注意点:
- 线程在进程的堆中分配了自己的tiddata内存结构。
- 传递给线程函数地址(initialcode )被保存在了tiddata内存块中,传递给线程函数的参数(argument)也保存在了tiddata内存块中
- _beginthreadex的确调用了CreateThread,这大概是操作系统创建新线程的唯一方法
- 我们看到了,在_createThread中的参数是_threadstartex而不是initialcode来执行新的线程,另外,传递给线程函数的参数是tiddata数据块而不是argument
- 成功返回CreateThread的线程句柄,操作失败了,返回NULL。
有兴趣的可以了解一下tiddata数据结构(在文件mtdll.h中的struct _tiddata中定义)。
哈哈,这说明了一个问题,如果有东西搞不定,有一个万能的办法,加一个中间层!
既然为新线程指定了一个tiddata数据结构,并对其进程了初始化,那么我们需要知道该结构如何跟线程关联起来的。我们发现CreateThread的线程函数是_threadstartex函数,so让我们看一下这个线程函数的定义吧:
/****_threadstartex() - New thread begins here**Purpose:* The new thread begins execution here. This routine, in turn,* passes control to the user's code.**Entry:* void *ptd = pointer to _tiddata structure for this thread**Exit:* Never returns - terminates thread!**Exceptions:********************************************************************************/static unsigned long WINAPI _threadstartex ( void * ptd ){ //指向tiddata的指针 _ptiddata _ptd; /* pointer to per-thread data */ /* 看一下ptd指向的数据块是否关联到了TLS(Thread Local Storage)中,若没有, * 则将ptd关联到线程的TLS中,另外再设置一下tiddata._tid线程id。 * 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; } // 这是一种情况,若是_ptd已经关联过了,那么使用ptd对_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);}tatic void _callthreadstartex( void){ _ptiddata ptd; /* pointer to thread's _tiddata struct */ // 这种是否了,ptd一定会存在的 /* 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 */}
下面是_threadstartex的一些注意点:
- 新线程从BasethreadStart函数执行,然后转移到_threadstartex。
- _threadstartex的参数是一个tiddata的数据块,这个数据块中保存了线程函数和传递给线程函数的参数的地址。
- TlsSetValue是系统函数,负责将一个值与调用线程联系起来。这称为线程本地存储器(TLS)。
- 必要的线程函数返回值被认为是线程的退出代码。注意,_threadstartex不会返回到BaseThreadStart中的,如果直接返回的话,那么tiddata内存块就被留在了内存中,因为EndThread不知道这个数据块的存在。所以线程退出函数为_endthreadex,所以在_threadstartex中的末尾就结束了线程。
那么,让我们看一下_endthreadex函数(同样在threadex.c文件中):
_ptiddata __cdecl _getptd_noexit( void); /* return address of per-thread CRT data - doesn't exit on malloc failure */void __cdecl _endthreadex ( unsigned retcode ){ _ptiddata ptd; /* pointer to thread's _tiddata struct */ HANDLE handle = NULL; 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);}
对于该函数,需要注意的是:
- 函数_getptd_noexit函数内部是调用调用TlsGetValue函数获取tiddata的地址
- 释放tiddata的内存,并且调用ExitThread函数撤销线程(线程没了),当然,莫忘记设置退出代码。
OK,还有一个值得讨论,若是错误调用了CreateThread,并且使用C/C++运行期函数的怎么办?
首先,我们找一下函数_errno的定义:
int * __cdecl _errno( void ){ _ptiddata ptd = _getptd_noexit(); if (!ptd) { return &ErrnoNoMem; } else { return ( &ptd->_terrno ); }}
可知,该函数先获取tiddata数据块,然而,在CreateThread中,并没有分配tiddata数据块。那么让我们再来看一下_getptd_noexit函数定义:
_ptiddata __cdecl _getptd_noexit ( void ){ _ptiddata ptd; DWORD TL_LastError; TL_LastError = GetLastError(); if ( (ptd = __crtFlsGetValue(__flsindex)) == NULL ) { /* * no per-thread data structure for this thread. try to create * one. */#ifdef _DEBUG extern void * __cdecl _calloc_dbg_impl( size_t, size_t, int, const char *, int , int *); if ((ptd = _calloc_dbg_impl(1, sizeof( struct _tiddata), _CRT_BLOCK, __FILE__ , __LINE__ , NULL )) != NULL ) {#else /* _DEBUG */ if ((ptd = _calloc_crt(1, sizeof( struct _tiddata))) != NULL) {#endif /* _DEBUG */ if (__crtFlsSetValue(__flsindex, (LPVOID)ptd) ) { /* * Initialize of per-thread data */ _initptd(ptd, NULL); ptd->_tid = GetCurrentThreadId(); ptd->_thandle = ( uintptr_t)(-1); } else { /* * Return NULL to indicate failure */ _free_crt(ptd); ptd = NULL; } } } SetLastError(TL_LastError); return(ptd);}
可知,该函数首先获取tiddata的地址,若是获取的为空(即调用CreateThread没有分配tiddata),那么则会在现场为调用线程分配一个tiddata数据块,并对其进行初始化。然后再将之与线程TLS关联起来。此后,只要线程再运行,tiddata块就能和线程绑定在一起。所以所有C/C++运行期函数就可以使用tiddata块。
看起来运行没问题吧,但是在线程退出时?ExitThread并不知道有tiddata的存在,所以线程结束后,这个tiddata数据块就驻留在了内存中,导致的内存泄露。
当然,你可以说:我可以在线程函数里面强制调用一下_exitthreadex()函数不就结了!的确,看起来这样确实可以解决内存泄露的问题,但是这不标准,搞不好会有奇奇怪怪的错误,同志们,还是老实一点吧。
所以,结论就是:
- 不要用CreateThread来创建线程了,使用_beginthreadex来创建吧。
我了个去,帖代码的文章就是篇幅长。明天继续,多线程的互斥问题~
0 0
- windows多线程基础(5):使用_beginthreadex创建线程(C/C++ Runtime Library考虑)
- 使用_beginThreadex创建多线程(C语言版多线程)
- 使用_beginThreadex创建多线程(C语言版多线程)
- 【Win32多线程】使用C runtime Library
- C Runtime Library(CRT Library)
- windows核心编程笔记第10篇 (线程创建的过程和基于C/C++运行库的_beginthreadex)
- 多线程第一篇:使用_beginthreadex创建线程
- C Runtime Library, C Runtime Library and Windows API
- 多线程同步(C++)C/C++ Runtime函数使用
- 使用_beginthreadex()创建线程
- 【转载】C Runtime Library(MSVCRT)来历
- C Runtime Library(MSVCRT)来历 .
- C Runtime Library(MSVCRT)来历
- C Runtime Library(MSVCRT)来历
- C Runtime Library(MSVCRT)来历
- C Runtime Library(MSVCRT)来历
- C++多线程(二)(_beginThreadex创建多线程)
- 使用_beginthreadex 创建线程并实现多线程同步
- hadoop动态增加和删除节点
- 堆、栈的区别 [经典]
- Axure使用心得分享
- hdu 5073 Galaxy (鞍山现场赛D题)
- UVA - 10714 Ants
- windows多线程基础(5):使用_beginthreadex创建线程(C/C++ Runtime Library考虑)
- 重拾c语言之动态内存分配
- Sicily 1028. Hanoi Tower Sequence
- Spring + activemq
- BEGINNING SHAREPOINT® 2013 DEVELOPMENT 第15章节--开发SP2013工作流应用程序 介绍工作流管理器
- bzoj1036: [ZJOI2008]树的统计Count [Link-Cut-Tree/树链剖分]
- 第一周总结
- C++ 智能指针详解
- 第9周 项目3-2 编程输出星图(b)