为什么要用C运行时库的_beginthreadex代替操作系统的CreateThread来创建线程?

来源自自1999年7月MSJ杂志的《Win32 Q&A》栏目

你也许会说我一直用CreateThread来创建线程,一直都工作得好好的,为什么要用_beginthreadex来代替CreateThread,下面让我来告诉你为什么。
回答一个问题可以有两种方式,一种是简单的,一种是复杂的。
如果你不愿意看下面的长篇大论,那我可以告诉你简单的答案:_beginthreadex在内部调用了CreateThread,在调用之前_beginthreadex做了很多的工作,从而使得它比CreateThread更安全。
OK,下面是复杂的回答,^_^:
微软在发布VC的同时附带了6个CRT库,下表列出了这些库的名称和详细描述:

Library Name
Description
LIBC.LIB
Statically linked library for single-threaded applications (this is the default library chosen when you create a new project).
LIBCD.LIB
Statically linked debug version of the library for single-threaded applications.
LIBCMT.LIB
Statically linked release version of the library for multithreaded applications.
LIBCMTD.LIB
Statically linked debug version of the library for multithreaded applications.
MSVCRT.LIB
Import library for dynamically linking the release version of the MSVCRT.DLL library. The library supports both single-threaded and multithreaded applications.
MSVCRTD.LIB
Import library for dynamically linking the debug version of the MSVCRT.DLL library. The library supports both single-threaded and multithreaded applications.

在VC 6和VS 2003中可以在下图中所示的项目进行设置:


此主题相关图片如下:
按此在新窗口浏览图片

此主题相关图片如下:
按此在新窗口浏览图片

为什么我们需要两个几乎相同的库来分别对待单线程和多线程程序?说起来也很简单,两个字——效率。让我们从头说起,标准CRT库出现于1970年左右,那时,线程的概念尚未出现在任何一个操作系统上。但是,线程毕竟是出现了,那好,让我们来看看下面这个例子,在这个例子中我们使用了CRT的全局变量errno:

BOOL fFailure = (system("NOTEPAD.EXE README.TXT") == -1);

if (fFailure) {
     switch (errno) {
     case E2BIG: // Argument list or environment too big
         break;

   case ENOENT: // Command interpreter cannot be found
       break;

   case ENOEXEC: // Command interpreter has bad format
       break;

   case ENOMEM: // Insufficient memory to run command
       break;
   }
}

设想这样的情况,当上面的代码执行到system函数之后,if声明之前的时候,操作系统打断了它,而转去执行进程中的另一个线程,而这个线程正好使用了会设置errno的某个CRT函数......于是,问题就出现了。
为了解决这个问题,每个线程需要自己的errno全局变量,而且还需要一些机制来使得它们使用它们自己的errno变量,而不是其他线程的。当然,errno只是“多线程不服症”的其中一个受害者,其他受害者还有:_doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt, _fcvt。
于是,为了让C和C++程序能够正常工作,必须创建一个数据结构,并把它与每一个线程关连起来,只有这样才能调用CRT库时不至于误入“他线程家园”。
那么系统怎么知道在创建一个新线程时分配这个数据块呢?回答是系统不知道,这一切责任都在你,只有你才能确保所有的事情正常完成。
是不是有点重任在肩的感觉?呵呵,不要紧,其你要做的和标题所说的一样,只需要调用_beginthreadex函数即可:

unsigned long _beginthreadex(void *security,
     unsigned stack_size,
     unsigned (*start_address)(void *), void *arglist,
     unsigned initflag, unsigned *thrdaddr);

_beginthreadex的参数列表与CreateThread一模一样,只是参数名与类型有少许差异罢了。这是因为Microsoft觉得CRT函数不应该对Windows的数据类型有任何依赖。两者返回
的东西也是一样的,所以即使你使用了CreateThread函数,要替换成_beginthreadex也是一件很容易的事情。
因为两者的数据类型不完全一致,所以我们需要作一些转换来避免编译器的抱怨,为了简化这项工作,你可以使用我所写的这个宏:

typedef unsigned (__stdcall *PTHREAD_START) (void *);

#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, \
     pvParam, fdwCreate, pdwThreadID)                \
       ((HANDLE) _beginthreadex(                     \
          (void *) (psa),                            \
          (unsigned) (cbStack),                      \
          (PTHREAD_START) (pfnStartAddr),            \
          (void *) (pvParam),                        \
          (unsigned) (fdwCreate),                    \
          (unsigned *) (pdwThreadID)))

注意_beginthreadex函数只存在于CRT库的多线程版本中,如果你链接到了一个单线程运行时库,链接器会毫不客气地报告“unresolved external symbol”错误。另外,还需要注意的是VS在创建新项目时默认选择的是单线程库,所以需要记得修改设置。
说了这么多,只是说了一些概念,至于_beginthreadex为什么要比CreateThread更好,还是需要事实来说话的,当然,程序员所说的事实,就是代码了,代码之前,了无秘密,所以下面让我们来看看CRT库的代码是怎样的。
首先,自然是主角人物_beginthreadex(你可以在THREADEX.C中找到它),因为没必要在这里重复写出源代码,所以我只给出伪代码版本的_beginthreadex:

unsigned long __cdecl _beginthreadex (
     void *psa,
     unsigned cbStack,
     unsigned (__stdcall * pfnStartAddr) (void *),
     void * pvParam,
     unsigned fdwCreate,
     unsigned *pdwThreadID) {

     _ptiddata ptd;         // Pointer to thread's data block
     unsigned long thdl;    // Thread's handle

     // Allocate data block for the new thread
     if ((ptd = calloccrt(1, sizeof(struct tiddata))) == NULL)
         goto errorreturn;

     // 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;

     // Create the new thread
     thdl = (unsigned long) CreateThread(psa, cbStack,
         _threadstartex, (PVOID) ptd, fdwCreate, pdwThreadID);
     if (thdl == NULL) {
         // Thread couldn't be created, cleanup and return failure
         goto error_return;
     }

     // Create created OK, return the handle
     return(thdl);

error_return:
     // Error: data block or thread couldn't be created
     _free_crt(ptd);
     return((unsigned long)0L);
}

_beginthreadex的代码中有几个地方需要重点注意:
首先每个线程会从CRT的堆上获得真正属于它自己的tiddata内存块。tiddata数据结构你可以在MTDLL.H中找到。传递给_beginthreadex的线程函数的地址被保存在tiddata内存块中。要传递给该线程函数的参数也被保存在这里。_beginthreadex接下来调用CreateThread,注意,这时CreateThread在新线程中执行的并不是pfnStartAddr函数,而是一个名为_threadstartex的函数。同时,传递给线程函数的参数也不是pvParam,而是tiddata结构的地址。最后,如果一切顺利将返回线程句柄,如果任何一个操作失败,将返回NULL。
现在,tiddata结构已经被分配并初始化完成,下面来看看该结构是如何关联到线程的。这次的对象是_threadstartex,同样也在THREADEX.C中,同样也给出伪代码:

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
    TlsSetValue(__tlsindex, ptd);

    // Save this thread ID in the tiddata block
    ((_ptiddata) ptd)->_tid = GetCurrentThreadId();

    // Initialize floating-point support (code not shown)

    // Wrap desired thread function in SEH frame to
    // handle runtime errors and signal support
    __try {
        // Call desired thread function passing it the desired parameter
        // Pass threads exit code value to _endthreadex
        _endthreadex(
          ( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) )
              ( ((_ptiddata)ptd)->_initarg ) ) ;
    }
    __except(_XcptFilter(GetExceptionCode(), GetExceptionInformation()){
        // The C-Runtime's exception handler deals with runtime errors
        // and signal support, we should never get it here.
        _exit(GetExceptionCode());
    }

    // We never get here, the thread dies in this function
    return(0L);
}

_threadstartex同样也有一些东西需要我们注意。新线程开始时会执行BaseThreadStart(位于Kernel32.DLL中),然后跳到_threadstartex。_threadstartex的唯一参数就是新线程的tiddata内存块地址。TlsSetValue完成了将tiddata结构与线程关联起来的目的(这里的tiddata结构被称为线程本地存储,TLS,顾名思义,就是属于每个线程自己的数据)。
在事实上的线程函数周围放置了一个结构化异常处理体(A structured exception handling frame)。这个处理体主要负责处理与运行时库有关的很多东西,比如运行时错误(像抛出但却没有被捕获的C++异常这类东西)和CRT的signal函数。这很重要,如果你使用CreateThread创建了线程,然后又调用了CRT的signal函数,那么signal函数将无法正常工作。
注意,这时还不能返回到BaseThreadStart,如果这样做,线程会死掉,退出码会正常设置,但tiddata内存块不会被销毁,这就会造成内存泄漏。为了防止泄漏,需要调用_endthreadex,并且将退出码传递给它。
_endthreadex同样也在THREADEX.C中,同样也给出伪代码:

void __cdecl _endthreadex (unsigned retcode) {
     _ptiddata ptd;    // Pointer to thread's data block

     // Cleanup floating-point support (code not shown)

     // Get the address of this thread's tiddata block
     ptd = _getptd();

     // Free the tiddata block
     _freeptd(ptd);

     // Terminate the thread
     ExitThread(retcode);
}

注意CRT的_getptd函数在内部调用了系统的TlsGetValue函数来获取对应线程的tiddata内存块地址,然后释放该内存块,最后调用ExitThread来真正销毁线程,当然是用上面所提到的退出码来调用。
我强烈建议你绝不要调用ExitThread来中止你的线程。最好也是最简单的办法就是让线程自己返回即可,让它自生自灭。ExitThread不仅徒增复杂,而且还会造成tiddata内存块泄漏。
Microsoft Visual C++项目组发现人们总是喜欢调用ExitThread,他们希望能尽可能的做到让程序不泄漏内存。所以如果你真的想要明确地退出线程,你也最好使用_endthreadex,虽然这也不太好。
OK,目前为止你应该对谁更好些的问题有了深入的了解,但是为什么调用CreateThread的程序仍然可以经年累月的正常运行呢?当线程调用一个需要tiddata结构的CRT函数时(大多数CRT函数是线程安全的,并不需要该结构),首先CRT函数试图获取线程的数据块的地址(通过调用TlsGetValue),然后,如果返回NULL,说明调用线程没有相关联的tiddata块,那么CRT函数马上为调用线程分配并初始化一个tiddata块,并将该内存块关联到线程(通过TlsSetValue),这样,该CRT函数以及其他CRT函数都可以使用该线程的tiddata块了(此即所谓“前人栽树后人乘凉”了,^_^)。
当然,如果说你的线程运行的时候一直没有问题是几乎不可能的。事实上,的确有一些问题需要说说。如果线程使用了CRT的signal函数,整个进程都会被中止,因为结构化异常处理体尚未准备好。同样,如果不调用_endthreadex来中止线程就会造成内存泄漏,如果使用_beginthreadex,当然会容易想到_endthreadex,但如果你习惯了使用CreateThread,是否还会想起_endthreadex,我表示极大的怀疑,而且CreateThread/_endthreadex的组合怎么看怎么让人别扭。
不要忘记开始的问题,接下来让我们再来看看效率问题。CRT库的多线程版本在某些函数里面放置了同步原语,比如malloc,为了保证堆不会被同时调用的malloc函数破坏,这不可避免地会对效率造成影响,C/C++的哲学我们不应忘记,“决不为自己没有用到的付出代价”,自然,我们无权要求单线程程序为多线程程序付出它们不该付出的代价,所以,开头的问题也有了答案。
上面所说的都是静态链接的CRT库,而CRT库的动态链接版本则被编写得更加通用,以便能够被任何运行的程序和DLL共享。正是基于这个原因,这个版本的库只存在多线程版本。因为CRT库是以DLL形式提供的,程序和DLL不需要包含CRT库的任何代码,自然尺寸也就更小。同时,如果Microsoft修正了CRT库DLL中的Bug,程序也就自然受益了。
终于该结束了,还是来几句总结吧:首先,如果你调用_beginthreadex,你会获得线程的句柄,句柄当然需要关闭,但_endthreadex并没有这么做。通常,是调用_beginthreadex的线程(很可能是主线程)来调用CloseHandle关闭不再需要的新线程的句柄。其次,如果你使用CRT函数,你只需要使用_beginthreadex即可。如果不使用,那么你可以只使用CreateThread。同样,如果只有一个线程(主线程)使用CRT,你也可以使用CreateThread;如果新创建的线程不使用CRT,那么你也不需要_beginthreadex和多线程CRT。

posted on 2006-12-05 12:27  sPhinX  阅读(6818)  评论(1编辑  收藏  举报

导航