在上一篇关于线程的讲解中,有提到一般我们都不应该直接调用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内存块。这样你的应用程序存在内存泄漏。