zoukankan      html  css  js  c++  java
  • windows主线程等待子线程退出卡死问题

    在windows下调用_beginthread创建子线程并获得子线程id(函数返回值),如果子线程很快退出,在主线程中调用WaitForSingleObject等待该线程id退出,会导致主线程卡死。需要修改_beginthread为_beginthreadex解决该问题。

    那么,_beginthread为何会导致WaitForSingleObject卡死,而_beginthreadex却不会呢?这需要查看两个函数的实现。

    历史原因

    由于C/C++的历史早于线程的出现,因此C/C++的函数并不都是线程安全的。如全局变量errno等。

    这就需要一种解决方案。一种方法是利用属于每个线程的数据块,该数据块不会被线程共享,而只能够用于线程自己,这样类似errno的情况便迎刃而解。

    此外,C/C++运行库针对特定函数做了改写,使其能够进行线程同步。如malloc函数,由于不能够多线程同时执行内存堆分配操作,因此多线程版本的运行库进行了线程同步处理。

    那么,如何让windows系统知道当我们创造新线程时,为我们分配属于线程的存储区呢?利用CreateThread函数并不行(C/C++运行库若获取不到存储器,会自动请求分配对应存储区,因此CreateThread函数实际也可以支持线程安全,但还有其他问题下面再说),因为他只是一个系统API,他不会知道你所写的是CC++代码。

    _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++线程安全做了那些事情。我们可以看到其函数定义。(VS2013路径为C:Program Files (x86)Microsoft Visual Studio 12.0VCcrtsrc hreadex.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,
                                (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 来讲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++运行库终止线程运行的函数,其调用是在上面提到的_callthreadstartex中,每次我们的线程执行完后会自动对其调用。其定义如下

    /***
    *_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

    在编写CC++程序时,要调用_endthreadex来结束线程。基于如下两个理由:

    1、ExitThread函数非C++函数,线程创建的C++对象不会得到析构。

    2、若线程中使用了ptd,ExitThread不会释放内存,造成内存泄露。

    CreateThread VS _beginthreadex

    一般的理由是,CreateThread有可能照成内存泄露。(如果使用了ptd内存,而CreateThread并不会在内部自动调用释放内存函数,但若链接的是C/C++运行库的dll版本,则其会在线程退出的DLL_THREAD_DETCH通知中释放内存)。

    不要调用的C/C++函数

    _beginthreadex和_endthreadex分别有两个比较老的版本:(VS2013路径为C:Program Files (x86)Microsoft Visual Studio 12.0VCcrtsrc hread.c)

    uintptr_t _beginthread( 
       void( __cdecl *start_address )( void * ),
       unsigned stack_size,
       void *arglist 
    );
    
     void _endthread( void );

    我们应该忘记这两个函数,不要调用它们。

    对于_beginthread函数,可以看出其函数参数是较少的,例如其中不包括安全属性,让我们对线程的控制力没有其增强版本多。

    同时,由于在_beginthread内部会调用_endthread函数,而该函数多此一举的会调用一次CloseHandle,来帮我们关闭线程句柄。

    void __cdecl _endthread (
            void
            )
    {
            _ptiddata ptd;           /* pointer to thread's _tiddata struct */
    
            ptd = _getptd_noexit();
            if (ptd) {
                /*
                 * Close the thread handle (if there was one)
                 */
                if ( ptd->_thandle != (uintptr_t)(-1) )
                        (void) CloseHandle( (HANDLE)(ptd->_thandle) );
    
                /*
                 * 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(0);
    
    }

    这个操作似乎友好,但实际会造成问题。例如下边代码

    HANDLE hThread = _beginthread(...);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);

    在真正调用WaitForSingleObject之前,_beginthread函数里的线程可能已经执行完毕,同时,_endthread会释放handle句柄。那么再调用WaitForSingleObject时,可能这时的hThread已经是一个无效句柄,导致函数调用失败,同理,对CloseHandle也是一样。

    参考:http://blog.csdn.net/u013378438/article/details/43447349

  • 相关阅读:
    Linux 下安装nodejs
    Linux 下安装JDK
    ubuntu 把软件源修改为国内源
    vi/vim 命令使用详解
    不同浏览器css引入外部字体的方式
    npx 命令介绍
    ICloud没有密码怎么注销?
    装修后才知道的79件事
    天翼宽带政企网关B2-1P 如何获得超级管理员账号?
    家庭治疗偏头痛
  • 原文地址:https://www.cnblogs.com/budapeng/p/5442112.html
Copyright © 2011-2022 走看看