为什么创建线程不用 CreateThread,而用 __beginthreadex

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 )
             {
             ...
             }
    ......
}
见于 CreateThread 那行。ptd 即是 _tiddata ,它是线程私有数据,ptd ->_thandle 保存了新线程在调用线程中的句柄。


比较了这四个线程结束函数之后,我们再回到上一个问题:既然非线程安全库函数在内部已经检查了 _tiddata 的存在与否,那么为何还是不建议使用 CreateThread 呢?这是因为,

CreateThread 并不会清除产生的 _tiddata ,因而会造成内存泄露,_tiddata 只会在 _endthreadex 和 _endthread 中回收。而 _endthread 已如上面所说,是有缺陷的,它自动递减了线程内核对象的引用计数,这可能不是我们希望的,我们希望更灵活的控制,做更多的事情。

所以,综合上面的分析,尽可能地在 C/C++ 中使用 _beginthreadex。







  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值