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_);
  1. _beginthreadex函数的参数列表与CreateThread函数的参数列表是相同的,但是参数名和类型并不完全相同,据说是VC的C/C++运行期库的开发者认为不应该对Windows的数据类型有任何依赖。同样,返回参数为新创建线程的句柄。
  2. 我们知道VC的运行库分为单线程静态链接库、多线程静态链接库、多线程动态链接库(我在VC2013里面草草秒了一眼,貌似没有单线程静态链接库了,但是静态多线程连接库貌似有好几个版本了)。_beginthreadex函数只存在多线程的版本中,在使用多线程编译的时候,VC编译器会加上/MT参数。
  3. 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()函数不就结了!的确,看起来这样确实可以解决内存泄露的问题,但是这不标准,搞不好会有奇奇怪怪的错误,同志们,还是老实一点吧。

所以,结论就是:
  1. 不要用CreateThread来创建线程了,使用_beginthreadex来创建吧。

我了个去,帖代码的文章就是篇幅长。明天继续,多线程的互斥问题~
0 0