• 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

  • 相关阅读:
    【反射】Java反射机制
    Composer教程之常用命令
    Composer教程之基础用法
    Composer教程之初识Composer
    Composer 的结构详解
    现代 PHP 新特性系列(七) —— 内置的 HTTP 服务器
    现代 PHP 新特性系列(一) —— 命名空间
    现代 PHP 新特性系列(二) —— 善用接口
    现代 PHP 新特性系列(三) —— Trait 概览
    现代 PHP 新特性系列(四) —— 生成器的创建和使用
  • 原文地址:https://www.cnblogs.com/budapeng/p/5442112.html
Copyright © 2020-2023  润新知