CreateThread是Window的API函数,所有线程的创建都必须调用这个函数,但是如果当你想在多线程环境下用上CRT的话会怎么样呢?
作为一个C/C++程序员,有谁可以拍着胸脯说,我永远也用不上CRT(C Run Time)吗?就算你不显式地使用CRT,你又怎么知道你调用的那些函数哪一个会用上CRT呢?CRT功能强大,而且有着非常悠久的历史,它在19世纪70年代就问世了。这样久远的历史造就了CRT的一个本质问题,多线程环境下,CRT能否正确执行?
当年,可没有一个人能够想象出线程是个什么玩意。于是CRT在当时就没有考虑过多线程的执行问题。从而引发了这样的一个事情:CRT中有很多全局变量,例如errno。这些全局变量可不具备多线程安全的特性,线程A在使用某个变量前,它的值就已经可能被线程B更改。为了解决这样的问题,微软的CRT就会为每一个线程创建一个与线程有关的数据块(非内核对象)来为各个线程存储这些全局变量。在这种环境下,两个问题就非常显眼。
1.谁来创建这个数据块;
2.谁来回收这个空间。
理性地思考一番后,大概每一个人都会觉得,当线程创建的时候就来创建这块空间,当线程执行完成后就回收这块空间是比较合理的方案。那么CreateThread会不会创建这块空间,ExitThread会不会回收这块空间呢?遗憾的是,CRT和WIN API不是一个东西,无论是CreateThread还是ExitThread都不会关心CRT的运行,因此它们不会创建,也不会回收这块空间。
这时候,你就开始需要使用CreateThread和ExitThread的替代品:_beginthreadex和_endthreadex。
Windows核心编程上,作者给出了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);
}
上面这段看起来乱七八糟的代码实际上干了两件事情:
1.申请一个与线程有关的数据块
2.启动线程
如果你够细心,可以发现该函数在调用CreateThread的时候,传进去的函数地址实际上不是你传给beginthreadex的那个函数地址,它启动的函数是一个叫做_threadstartex的东西,并且传递给它的参数也不是你传给beginthreadex的参数,而是ptd,ptd正是前面所申请的那个与线程有关的数据块。
根据CreateThread内部调用的那个RtlUserThreadStart的尿性,聪明的人也一下就该知道这个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.
((_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());
}
}
上面的代码如果你没兴趣看,那么我告诉你它大概干了什么:
1.它把与线程有关的那个数据块放到了TLS中(线程局部存储区)
2.它有启动了另一个函数叫callthreadstartex的函数,这个函数取出TLS中存储的块,获得其中的函数地址(你传给beginthreadex里的那个),还有它的参数,在endthreadex里调用该函数,从而使得函数的返回值是endthreadex的参数,从而设定了线程的退出码并保证线程函数正确结束而释放相应的空间。
看到这里,你仿佛看到了CreateThread里面所使用的“先进”科技。 那么,留给我们的问题还有一个没有解决,那就是,现在线程结束了,线程函数返回了,咱们之前说好的那个线程数据块的回收工作谁来搞?
这个问题的答案就是,显然由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);
}
看到这条语句了么?
if (ptd != NULL)
_freeptd(ptd);
线程相关的块在线程退出的时候被清理掉了。
清理完成后,endthreadex调用ExitThread真正的结束了线程。
再次整理一下思路,我们得到以下的结论:
beginthreadex先创建一个块,然后把这个块放到TLS里,线程函数从TLS中取出真正要执行的函数地址和参数执行,返回值交给endthreadex用来设定退出码。
在endthreadex里,线程相关的块被删除,同时它调用了ExitThread来实现线程的退出。
这样,我们也就知道为什么该由_beginthreadex来替换掉CreateThread的调用。
不过,有人会说,我用了半辈子的CreateThread了,我也没出现过什么CRT调用的问题啊,我就不改又会怎么样呢?
Jeffrey RichterandChristophe Nasarre(windows核心编程作者) 说,如果通过CreateThread启动的线程调用了CRT,在CRT的调用中又需要使用线程数据块的话,线程会自动创建一个数据块。从而保证了CRT调用的正确性(多线程安全)。
但是:读过我前一篇线程内部细节的人都会知道,CreateThread通过调用ExitThread来结束线程,从而根本没有调用过endthreadex,于是你申请出来的那个块根本就无法回收,成了LEAK的资源。
同样,如果在线程函数中调用了Exit或者Terminate来关闭线程,线程的块和C/C++临时变量也无法回收。因此结束线程的最好方式是让线程函数自己返回。
此外,注意_callthreadstartex函数(就是实际执行了线程函数的那个家伙)中,有这样一段异常处理:
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation()))
这段异常处理用来处理CRT异常和CRT中singal函数的调用,如果你用CreateThread来创建线程,那么可见这段异常处理是没有的~呵呵~~~~
-------------------------------------
最后注意,不要再使用beginthread这个函数了,看清标题中的函数多一个ex有木有!!!!
beginthread这个函数很2,我为你介绍一下它2在什么地方:
1.beginthread开启的线程,如果线程返回的太快,那么beginthread将返回一个错误的句柄,为什么呢?因为:
2.beginthread调用了endthread,endthread调用了closehandle来关闭线程句柄。
于是你想一下,线程句柄的引用数是2,线程退出了,-1,beginthread里调用了endthread(跟CreateThread差不多的方式),于是又-1,句柄已经无效了,线程内核对象都已经摧毁了!!!
因此记住,使用beginthreadex,不要使用beginthread,同样适用于endthread,用endthreadex