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,他不會知道你所寫的是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++線程安全做了那些事情。我們可以看到其函數定義。(VS2013路徑為C:\Program Files (x86)\Microsoft Visual Studio 12.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,
                            (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

在編寫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分別有兩個比較老的版本:(VS2013路徑為C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src\thread.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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM