在上一篇关于线程的讲解中,有提到一般我们都不应该直接调用CreateThread函数去创建新线程,而是调用_beginthreadex函数创建新线程。
以下是_beginthreadex函数的伪代码:
uintptr_t __cdecl _beginthreadex ( void *psa, unsigned cbStackSize, unsigned (__stdcall * pfnStartAddr) (void *), void * pvParam, unsigned dwCreateFlags, unsigned *pdwThreadID) { _ptiddata ptd; // Pointer to thread's data block uintptr_t thdl; // Thread's handle // Allocate data block for the new thread. if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) goto error_return; // Initialize the data block. initptd(ptd); // Save the desired thread function and the parameter // we want it to get in the data block. ptd->_initaddr = (void *) pfnStartAddr; ptd->_initarg = pvParam; ptd->_thandle = (uintptr_t)(-1); // Create the new thread. thdl = (uintptr_t) CreateThread((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex, (PVOID) ptd, dwCreateFlags, pdwThreadID); if (thdl == 0) { // Thread couldn't be created, cleanup and return failure. goto error_return; } // Thread created OK, return the handle as unsigned long. return(thdl); error_return: // Error: data block or thread couldn't be created. // GetLastError() is mapped into errno corresponding values // if something wrong happened in CreateThread. _free_crt(ptd); return((uintptr_t)0L); }
关于_beginthreadex函数我们需要注意:
- 每个线程从C/C++运行库堆中获取自己的_tiddata内存块。
- 传递给_beginthreadex的线程函数地址记录在_tiddata内存块中。
- _beginthreadex函数内部调用了CreateThread函数,因为这是操作系统知道的创建新线程的唯一方法。
- 当CreateThread函数被调用时,它会被告知要开始执行_threadstartex函数(而不是pfnStartAddr)去开始一个新线程。同时也需要注意,此时传递过去的参数是_tiddata结构体地址而不是pvParam.
- 如果一切顺利的话,就会像CreateThread函数一样返回线程句柄。如果操作失败,则返回0值。
////////////////////////////////////////////////////////////////
上面提到了重要的_threadstartex函数,下面是它的伪代码:
static unsigned long WINAPI _threadstartex (void* ptd) { // Note: ptd is the address of this thread's tiddata block. // Associate the tiddata block with this thread so // _getptd() will be able to find it in _callthreadstartex. TlsSetValue(__tlsindex, ptd); // Save this thread ID in the _tiddata block. //Windows via C/C++, Fifth Edition by Jeffrey Richter and Christophe Nasarre ((_ptiddata) ptd)->_tid = GetCurrentThreadId(); // Initialize floating-point support (code not shown). // call helper function. _callthreadstartex(); // We never get here; the thread dies in _callthreadstartex. return(0L); } static void _callthreadstartex(void) { _ptiddata ptd; /* pointer to thread's _tiddata struct */ // get the pointer to thread data from TLS ptd = _getptd(); // Wrap desired thread function in SEH frame to // handle run-time errors and signal support. __try { // Call desired thread function, passing it the desired parameter. // Pass thread's exit code value to _endthreadex. _endthreadex( ((unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr)) (((_ptiddata)ptd)->_initarg)) ; } __except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){ // The C run-time's exception handler deals with run-time errors // and signal support; we should never get it here. _exit(GetExceptionCode()); } }
关于_threadstartex函数,需要注意:
- 一个新线程最开始执行RtlUserThreadStart函数,然后跳到_threadstartex。
- 新线程的_tiddata数据块是_threadstartex函数唯一的参数。
- TlsSetValue函数是一个将一个数值关联到线程的系统函数。
- 在无参数的_callthreadstartex函数中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个SEH帧处理许多鱼运行库相关的事情(比如运行错误等)。
- 接下来就执行预期函数pfnStartAddr,并传递预期参数pvParam。pfnStartAddr和pvParam之前被记录在_tiddata块中。
- 预期的线程函数返回值就是线程的退出码。值得注意的是:_callthreadstartex不仅仅是返回至_threadstartex然后再返回到RtlUserThreadStart;如果真的这样做,线程会消亡,退出码也能正确设置,但是线程的_tiddata内存块不会被销毁。这样将会导致你的程序出现内存泄漏。为了防止这种情况,就需要调用C/C++运行库函数--_endthreadex。
////////////////////////////////////////////////////////////////
下面就来介绍_endthreadex函数,下面就是其伪代码:
void __cdecl _endthreadex (unsigned retcode) { _ptiddata ptd; // Pointer to thread's data block // Clean up floating-point support (code not shown). // Get the address of this thread's tiddata block. ptd = _getptd_noexit (); // Free the tiddata block. if (ptd != NULL) _freeptd(ptd); // Terminate the thread. ExitThread(retcode); }
对于_endthreadex函数,我们需要注意的是:
- 当你的线程函数返回时,_beginthreadex函数会调用_endthreadex函数。
- C运行库的_getptd_noexit函数内部调用操作系统的TlsGetValue函数,TlsGetValue函数获取调用线程的tiddata内存块地址。
- 这个_tiddata数据块之后被释放,然后调用操作系统ExitThread函数去真正销毁线程。
在实际编程过程中,我们也不应该直接调用ExitThread函数去退出一个线程,而是调用_endthreadex函数。原因有两个:
- 调用ExitThread会杀死线程,而不会让执行的线程函数返回。如果线程函数没有返回,则函数内的C++对象就不会被销毁。
- 调用ExitThread退出程序后,不会回收_tiddata内存块。这样你的应用程序存在内存泄漏。