CreateThread 是一个 Windows API,它不属于 C/C++ 运行库函数,而是 windows 下的系统调用。也就是说,在 windows 上创建线程的任务最后必然会落到 CreateThread 这个函数上,那么,为何有此一说:在 C/C++ 中创建线程不用 CreateThread,而用 _beginthreadex 呢?
这与 _beginthreadex 的实现是有关系的,_beginthreadex 是 C/C++ 库函数,它对 CreateThread 进行了包装。往往一个系统调用做的是最精简,不易变的事情。_beginthreadex 在此基础上做了别的事情。回顾 C 语言的历史,它被发明于 70 年代,那个时候,操作系统中还没有线程的概念,更不谈多线程。直到后来多 CPU 机器的出现,顺应时代,产生了多线程以便更充分地利用 CPU,提高程序效率。这里多说一句,进程由操作系统产生,多个任务同时存在时,操作系统负责切换进程运行。有了多线程后,我们可以把进程看作是一个中间层,它包裹线程,真正运行的是线程,而不是之前的进程。进程的管理由操作系统管理,线程的管理可以看作由进程管理。这一演变的历史,我们可以清晰地看出,为了提高效率产生的多线程,其实也不是什么新的概念。我们可以看作是,将进程变成了中间层,介于操作系统与线程之间。再次应了一句话,任何问题可以通过引入中间层解决!
回到上面的话题, C 语言设计之初并没有多线程的概念,这直接导致了 C 运行库有些函数和全局变量不是线程安全的,如 errno, _doserrno, strtok, _wcstok, strerror, ....等等,当它们被应用于多线程中时,可能引发变量污染,函数不可重入的问题(即一个函数多次调用,得到的状态不一致)。
C++ 完全兼容 C ,也存在这个问题,那么这个问题在多线程大行其道的今天,怎么解决呢?
如你所想,也是引入一个中间层。以前(没有线程概念的年代)运行库的隔离单位是进程(即在一个进程里面保证不会出现“非线程安全”问题),现在我们要让这些非线程安全的因素变为线程安全,即是要将这些非线程安全的内容隔离到每一个线程里面,不让它在一个进程里面共享即可。
这个中间层是一个数据结构,名为 _tiddata ,它作为线程私有数据,即 TLS (thread local storage)。将原本在一个进程里面共享的非线程安全的变量,移至每个线程里面。以后在某一个线程里面访问这些变量的时候,就访问的是本线程的变量。
以访问 C 全局变量 errno 为例,运行库很有可能有如下的实现:
#define errno (getErrno())
int getErrno()
{
_tiddata *ptd = getCurrentThreadPtd();
if(NULL == ptd)
{
return ERRNO; //原 C 运行库的 errno 全局变量
}
else
{
return ptd->ERRNO;
}
}
从上面的代码我们可以看出一点,非线程安全的全局变量,增加一个中间层函数(getErrno),于变量与以前的获得方式之间。如此便可兼顾原来的运行库代码与线程安全。
实际上,上面的代码要适当修改。考虑用户写出以下代码:int *p = &error; 则编译会不通过。对返回值取地址是不合法的。正确的实现很有可能是这样:
#define errno (*getErrno())
int* getErrno()
{
_tiddata *ptd = getCurrentThreadPtd();
if(NULL == ptd)
{
return &ERRNO; //原 C 运行库的 errno 全局变量
}
else
{
return &(ptd->ERRNO);
}
}
很明显,在 C/C++ 中每个线程在创建之时,必定要初始化一个 _tiddata,才能保证线程安全。那么, CreateThread 有没有这个步骤呢?答案是,肯定没有,以后也不会有!
因为,操作系统与运行库是一对多的关系,在 windows 系统上,有 C/C++ 运行库,还有 Java 运行库,PHP 运行库,等等。一种语言就有一个运行库。操作系统并不知道这些运行库的线程安全与否,它们不一定都需要额外的线程安全保证,基于这点,操作系统并不想付出可能不必要的劳动。所以,线程安全的保证必须要交给运行库自己来保证。
所以 CreateThread 这个系统调用中不可能进行线程安全的保证。对于 C/C++ 来说,它必定不会为新创建的线程分配 _tiddata,这个活儿最终要落到运行库函数 _beginthreadex 上。它将先为要创建的线程(在堆上)分配并初始化 _tiddata,并将这个数据放于要创建的线程的 TLS 中。
基于上面的认识,我们得知,调用 _beginthreadex 比 CreateThread 更“线程安全”一些。
但你可能会反驳,“不一定”。的确,如果我们在每个非线程安全的库函数之中检测当前线程的 _tiddata 是否存在,如果不存在,即创建并初始化,以后调用非线程安全函数都忽略这个动作。
真实的实现确实这样做了,因为运行库无法阻止用户使用 CreateThread 创建线程,但运行库必须要尽最大努力防止出现非线程安全的调用。
按上面的解释,_beginthreadex 和 CreateThread 应该是一样的了啊,前者是创建线程之前就保证线程安全,后者是创建线程后运行时保证线程安全。于是,我们就得深入挖掘,看这几个函数,ExitThread, TerminateThread, _endthreadex, _endthreade。
1. ExitThread 用于退出自身线程,它保证线程的堆栈会被销毁。但调用 ExitThread 的块中的 C++ 栈上对象得不到析构。如
void func()
{
Dog d;
Person p;
ExitThread();//一旦调用,立即结束调用线程,清理线程栈,并不返回到 func 函数中,因此,func 函数中的 d, p 都得不到析构,可能会造成内存泄露。
}
ExitThread 第二个副作用是,被分配的 _tiddata 得不到回收,因为它总是在 _endthreadex 中被回收,调用 ExitThread 之后,线程栈被销毁,再也没有机会调用 _endthreadex 了,所以会造成内存泄露。
2. TerminateThread 这是一个更暴力的线程结束函数,其更甚 ExitThread 的两个地方是:
线程栈得不到销毁,且它能结束同一个进程中的其它线程,它能接受一个线程的句柄作为参数。
TerminateThread 不销毁线程栈目的在于,其它线程可能会引用“被杀死线程”的栈上的值,这样其它线程就还可以正常工作。所以要十分注意 TerminateThread 的调用时机。
此外,TerminateThread 的调用是异步的,它只是向操作系统提出申请要结束一个线程,它被调用后立即返回,并不保证返回之时线程已结束。可以调用 WaitForSiingleObject 来确定“被终止的线程”终止了。使用 TerminiateThread 结束一个线程时,该线程载入的 Dll 不会收到通知。
3._endthreadex 它先回收 _tiddata ,再调用 ExitThread。_beginthreadex 内部自动调用 _endthreadex。
4._endthread 回收 _tiddata,再调用 CloseHandle,关闭当前线程的引用,再调用 ExitThread。_beginthread 内部自动调用 _endthread。
一般情况下,_beginthread 和 _beginthreadex 两者的使用如下:
_beginthread(...);
HANDLE th = _beginthreadex(...);
_beginthread 一般不用 HANDLE 把新线程句柄保存下来,因为 _beginthread 产生的新线程一旦结束,则会发现如下两件事:
a).线程结束一定会调用 ExitThread ,它会使新线程的内核对象使用计数递减 1.
b)._endthread 会调用 CloseHandle 关闭新线程句柄,于是线程内核对象使用计数再次递减。
也就是说,只要 _beginthread 产生的线程结束,则线程的内核对象引用计数就变为 0(新线程创建之前引用计数为 2,递减两次即为 0),此时任何指向线程句柄的变量都是无效的。
这里有个朦胧的问题需要提一下,上面的 b) 中说到了 _endthread 会 CloseHandle,此时 CloseHandle 的参数是什么呢 ?如果在当前线程(新线程)中调用 GetCurrentThread 则值为一个定值(伪句柄),而 _beginthread 设计的思想是关闭新线程在调用线程(父线程)中的句柄,那么,它是怎么做到的呢 ?通过查看 thread.c 代码,发现在 _beginthread 里保存了 CreateThread 返回的新线程句柄(也即,调用线程中新线程的句柄)。摘录如下:
_MCRTIMP uintptr_t __cdecl _beginthread (
void (__CLRCALL_OR_CDECL * initialcode) (void *),
unsigned stacksize,
void * argument
)
{
......
_initptd(ptd, _getptd()->ptlocinfo);
ptd->_initaddr = (void *) initialcode;
ptd->_initarg = argument;
#if defined (_M_CEE) || defined (MRTDLL)
if(!_getdomain(&(ptd->__initDomain)))
{
goto error_return;
}
#endif /* defined (_M_CEE) || defined (MRTDLL) */
/*
* Create the new thread. Bring it up in a suspended state so that
* the _thandle and _tid fields are filled in before execution
* starts.
*/
if ( (ptd->_thandle = thdl = (uintptr_t)
CreateThread( NULL,
stacksize,
_threadstart,
(LPVOID)ptd,
CREATE_SUSPENDED,
(LPDWORD)&(ptd->_tid) ))
== (uintptr_t)0 )
{
...
}
......
}
比较了这四个线程结束函数之后,我们再回到上一个问题:既然非线程安全库函数在内部已经检查了 _tiddata 的存在与否,那么为何还是不建议使用 CreateThread 呢?这是因为,
CreateThread 并不会清除产生的 _tiddata ,因而会造成内存泄露,_tiddata 只会在 _endthreadex 和 _endthread 中回收。而 _endthread 已如上面所说,是有缺陷的,它自动递减了线程内核对象的引用计数,这可能不是我们希望的,我们希望更灵活的控制,做更多的事情。
所以,综合上面的分析,尽可能地在 C/C++ 中使用 _beginthreadex。
本文详细阐述了C/C++中线程安全问题及BeginThreadEx函数的作用,对比了BeginThreadEx与CreateThread在创建线程及线程安全方面的差异,介绍了线程局部存储(TLS)机制及其在解决线程安全问题中的应用,以及ExitThread、TerminateThread等线程结束函数的特点与使用场景。
1621

被折叠的 条评论
为什么被折叠?



