WINDOWS-API:关于线程CreateThread,_beginthead(_beginthreadex),AfxBeginThread

时间:2022-11-08 20:03:32

在Windows的多线程编程中,创建线程的函数主要有

1.CreateThread

2._beginthead(_beginthreadex)

3.AfxBeginThread

那么它们之间有什么联系与区别呢?当我需要创建一个线程时该用哪个函数呢?

下面先介绍各个函数的用法:

CreateThread:函数原型:

HANDLE WINAPI CreateThread(

_in LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向一个LPSECURITY_ATTRIBUTES结构的指针决定返回的句柄能否被继承,lpThreadAttributes空,不能被继承。

_in SIZE_T dwStackSize,                      //初始化的堆栈大小,以字节为单位。如果为0,使用默认的大小(1MB)。

_in LPTHREAD_START_ROUTINE lpStartAddress,   //函数的入口地址,是一个函数指针。 函数原型:DWORD WINAPI ThreadProc( [in]  LPVOID  lpParameter);

_in LPVOID lpParameter,                      //一个指针,被传递到线程函数里。

_in DWORD dwCreationFlags,       //线程创建的标志。如果为CREATE_SUSPENDED,那么需要使用ResumeThread函数来激活线程函数,如果为0,线程函数立刻执行。

_out LPDWORD lpThreadId          //一个指向线程id的指针,如果为空,线程id不被返回。

);

返回值:

  1:如果函数成功执行,返回值将是这个新线程的句柄。如果失败,返回值是NULL。

  2:当线程函数的起始地址无效(或者不可访问)时,CreateThread函数仍可能成功返回。如果该起始地址无效,则当线程运行时,异常将发生,线程终止,并返回一个错误代码,可以使用GetLastError获取。

说明:

  1:如果线程函数return,返回值会隐式调用ExitThread函数,可以使用GetExitCodeThread函数获得该线程函数的返回值。

  2:使用CreateThread创建的线程具有THREAD_PRIORITY_NORMAL线程优先级。可以使用GetThreadPriority和SetThreadPriority函数获取和设置线程优先级值。

  3:当一个线程结束时,这个线程的对象将获得有信号状态,使得任何等待这个对象的线程都能够成功并继续执行下去。

  4:系统中的线程对象一直存活到线程结束,并且所有指向它的句柄都需要通过调用CloseHandle关闭。

  5:如果一个线程调用了CRT,应该使用_beginthreadex 和_endthreadex(需要使用多线程版的CRT)。

  例:

#include <stdio.h>
#include <windows.h> DWORD WINAPI ThreadPorc(LPVOID pM)
{
printf("子线程的ID号为:%d\n子线程输出hello world!\n",GetCurrentThreadId());
return ;
} int _tmain(int argc, _TCHAR* argv[])
{
printf("最简单的创建线程的实例\n"); HANDLE handle = CreateThread(NULL,,ThreadPorc,NULL,,NULL);
WaitForSingleObject(handle,INFINITE); system("pause");
return ;
}

  WaitForSingleObject函数功能:等待函数 – 使线程进入等待状态,直到指定的内核对象被触发。

  函数原形:

DWORDWINAPIWaitForSingleObject(

    HANDLEhHandle,

    DWORDdwMilliseconds

  );

  函数说明:

    第一个参数为要等待的内核对象。

    第二个参数为最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待。

    因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行。

  函数返回值:

    在指定的时间内对象被触发,函数返回WAIT_OBJECT_0。超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT。传入参数有错误将返回WAIT_FAILED

_beginthread与_beginthreadex:

_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。

函数原型:

uintptr_t _beginthread(

   void( *start_address )( void * ),//线程函数的入口地址。对于_beginthread,线程函数的调用约定是_cdecl。对于_beginthreadex,线程函数的调用约定是_stdcall。

   unsigned stack_size,             //线程堆栈大小,可以为0。

   void *arglist                    //传递给线程函数的参数,可以为NULL。

);
uintptr_t _beginthreadex(

   void *security,                       //线程安全属性。

   unsigned stack_size,                  //线程堆栈大小,可以为0。

   unsigned ( *start_address )( void * ),//线程函数的入口地址。对于_beginthread,线程函数调用约定是_cdecl。对于_beginthreadex,线程函数调用约定是_stdcall。

   void *arglist,                        //传递给线程函数的参数,可以为NULL。

   unsigned initflag,                    //线程创建的初始标志。为CREATE_SUSPENDED则挂起线程,使用ResumeThread激活线程,为0则立即执行。

   unsigned *thrdaddr                    //线程Id。

);   

返回值:

  1:如果成功,将会返回一个新的线程句柄。然而,如果线程函数执行的很快,_beginthread可能得到一个非法的句柄。

  2:如果失败,_beginthread返回-1,此时errno变量将被设置。_beginthreadex返回0,此时errno和_doserrno都被设置。

说明:

  1:_beginthread函数的线程入口函数必须使用_cdecl调用约定。_beginthreadex函数的线程入口函数必须使用_stdcall调用约定

  2:使用_beginthreadex比使用_begingthread更加安全。因为_beginthread的线程函数可能执行很快,这时可能会返回一个非法的句柄。

  3:_endthread将会自动的关闭线程句柄,然而_beginthreadex不会,需要使用CloseHandle现实的关闭句柄。所以_beginthreadex函数可以用WaitForSingleObject    函数来获取线程对象来进行同步。

  4:一个连接Libcmt.lib的可执行文件,不要调用ExitThread函数,这个函数会阻止系统的运行时回收已分配的资源。使用_endthread and _endthreadex可以回收已分    配的资源然后再调用ExitThread.

  5: 可以调用_endthread和_endthreadex显示式结束一个线程。然而,当线程函数返回时,_endthread和_endthreadex 被自动调用。endthread和_endthreadex    的调用有助于确保分配给线程的资源的合理回收。

  6:当_beginthread和_beginthreadex被调用时,操作系统自己处理线程栈的分配。如果在调用这些函数时,指定栈大小为0,则操作系统 为该线程创建和主线程大小一    样的栈。如果任何一个线程调用了abort、exit或者ExitProcess,则所有线程都将被终止。

  7:对于使用C运行时库里的函数的线程应该使用_beginthread和_endthread这些C运行时函数来管理线程,而不是使用CreateThread和ExitThread。否则,当调      用ExitThread后,可能引发内存泄露。

  8:必须使用多线程版的 C run_time libraries.

//子线程报数
#include <stdio.h>
#include <process.h>
#include <windows.h>
int g_nCount;
//子线程函数
unsigned int __stdcall ThreadFun(PVOID pM)
{
g_nCount++;
printf("线程ID号为%4d的子线程报数%d\n", GetCurrentThreadId(), g_nCount);
return ;
}
//主函数,所谓主函数其实就是主线程执行的函数。
int main()
{
printf(" 子线程报数 \n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); const int THREAD_NUM = ;
HANDLE handle[THREAD_NUM]; g_nCount = ;
for (int i = ; i < THREAD_NUM; i++)
handle[i] = (HANDLE)_beginthreadex(NULL, , ThreadFun, NULL, , NULL);
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
return ;
}

显示结果从1数到10,看起来好象没有问题。

答案是不对的,虽然这种做法在逻辑上是正确的,但在多线程环境下这样做是会产生严重的问题。

#include <stdio.h>
#include <process.h>
#include <windows.h>
volatile long g_nLoginCount; //登录次数
unsigned int __stdcall Fun(void *pPM); //线程函数
const int THREAD_NUM = ; //启动线程数
unsigned int __stdcall ThreadFun(void *pPM)
{
Sleep(); //some work should to do
g_nLoginCount++;
Sleep();
return ;
}
int main()
{
g_nLoginCount = ; HANDLE handle[THREAD_NUM];   
for (int i = ; i < THREAD_NUM; i++)
handle[i] = (HANDLE)_beginthreadex(NULL, , ThreadFun, NULL, , NULL); WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);
return ;
}

  程序输出的结果好象并没什么问题增加点用户来试试,现在模拟50个用户登录,为了便于观察结果,在程序中将50个用户登录过程重复20次,代码如下:

#include <stdio.h>
#include <windows.h>
volatile long g_nLoginCount; //登录次数
unsigned int __stdcall Fun(void *pPM); //线程函数
const DWORD THREAD_NUM = ;//启动线程数
DWORD WINAPI ThreadFun(void *pPM)
{
Sleep(); //some work should to do
g_nLoginCount++;
Sleep();
return ;
}
int main()
{
printf(" 原子操作 Interlocked系列函数的使用\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //重复20次以便观察多线程访问同一资源时导致的冲突
int num= ;
while (num--)
{
g_nLoginCount = ;
int i;
HANDLE handle[THREAD_NUM];
for (i = ; i < THREAD_NUM; i++)
handle[i] = CreateThread(NULL, , ThreadFun, NULL, , NULL);
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);
}
return ;
}

  明明有50个线程执行了g_nLoginCount++;操作,但结果输出是不确定的,有可能为50,但也有可能小于50。 因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作。

  因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作——即不可打断性,一个线程在执行原子操作时,其它线程必须等待它完成之后才能开始执行该原子操作。这种涉及到硬件的操作会不会很复杂了,幸运的是,Windows系统为我们提供了一些以Interlocked开头的函数来完成这一任务(下文将这些函数称为Interlocked系列函数)。

下面列出一些常用的Interlocked系列函数:

1.增减操作

LONG__cdecl  InterlockedIncrement(LONG volatile* Addend);

LONG__cdecl  InterlockedDecrement(LONG volatile* Addend);

返回变量执行增减操作之后的值。

LONG__cdec  InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);

返回运算后的值,注意!加个负数就是减。

2.赋值操作

LONG__cdecl  InterlockedExchange(LONG volatile* Target, LONGValue);

Value就是新值,函数会返回原先的值。

在本例中只要使用InterlockedIncrement()函数就可以了。将线程函数代码改成:

DWORD WINAPI ThreadFun(void *pPM)
{
Sleep();//some work should to do
//g_nLoginCount++;
InterlockedIncrement((LPLONG)&g_nLoginCount);
Sleep();
return ;
}

  因此,在多线程环境下,我们对变量的自增自减这些简单的语句也要慎重思考,防止多个线程导致的数据访问出错。更多介绍,请访问MSDN上Synchronization Functions这一章节,地址为 http://msdn.microsoft.com/zh-cn/library/aa909196.aspx

AfxBeginThread:函数原型:

CWinThread* AfxBeginThread(

   AFX_THREADPROC pfnThreadProc,           //线程函数的入口地址。函数原型:UINT __cdecl MyControllingFunction( LPVOID pParam );
LPVOID pParam, //传递给线程函数的参数,可以为0。
int nPriority = THREAD_PRIORITY_NORMAL, //线程优先级。
UINT nStackSize = , //指明线程堆栈的大小,以字节为单位,可以为0。
DWORD dwCreateFlags = , //线程创建标志。
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程安全属性。
);
CWinThread* AfxBeginThread(

CRuntimeClass* pThreadClass,            //线程函数的入口地址。函数原型:UINT __cdecl MyControllingFunction( LPVOID pParam );
int nPriority = THREAD_PRIORITY_NORMAL, //线程优先级。
UINT nStackSize = , //指明线程堆栈的大小,以字节为单位,可以为0。
DWORD dwCreateFlags = , //线程创建标志。
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程安全属性。
);

返回值:

如果成功则返回一个指针指向线程对象,否则为NULL。

说明:

可以调用AfxEndThread来终止线程或者return。

下面来介绍下这几个函数的联系与区别:

CreateThread:

  CreateThread是Windows的API函数,提供操作系统级别的 创建线程的操作。_beginthread(及_beginthreadex)与AfxBeginThread的底层实现都调用了CreateThread函数。

  CreateThread函数没有考虑到下面二点:

  (1)C Runtime中需要对多线程进行记录和初始化,以保证C函数库工作正常(典型的例子就是strtok函数)

  (2)MFC也需要知道新线程的创建,也需要做一些初始化工作。

所以,在不调用MFC和CRT的函数时,可以用CreateThread创建线程,其它情况不要使用。

AfxBeginThread:

MFC中线程创建的函数,首先创建了相应的CWinThread对象,然后调用CWinThread::CreateThread,在CWinThread::CreateThread中完成了对线程对象的初始化工作,然后,调用_beginthreadex创建线程。注意不要在一个MFC程序中使用_beginthreadex()或CreateThread()。

_beginthread和_beginthreadex: (实现文件分别是thread.c和threadex.c)

是MS对C Runtime库的扩展SDK函数,首先对C Runtime库做了一些初始化的工作,以保证C Runtime库工作正常。然后,调用CreateThread真正创建线程。

若要使多线程C和C++程序能够正确地运行,必须创建一个数据结构,并将它与使用C/C++运行期库函数的每个线程关联起来。当你调用C/C++运行期库时,这些函数必须知道查看调用线程的数据块,这样就不会对别的线程产生不良影响。

1.每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构。

2.传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。传递给该函数的参数也保存在该数据块中。(指向tiddata结构的指针会作为一个TLS保存起来)

3._beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法。

4.当调用CreatetThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。还有,传递给线程函数的参数是tiddata结构而不是pvParam的地址。

5.如果一切顺利,就会像CreateThread那样返回线程句柄。如果任何操作失败了,便返回 NULL。

总结:

1:CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。

2:用_beginthreadex()、_endthreadex函数应该是最佳选择,且都是C Run-time Library中的函数,函数的参数和数据类型都是C Run-time Library中的类型,这样在启动线程时就不需要进行Windows数据类型和C Run-time Library中的数据类型之间的转化,从而减低了线程启动时的资源消耗和时间的消耗。

3:MFC也是C++类库(只不过是Microsoft的C++类库,不是标准的C++类库),在MFC中也封装了new和delete两中运算符,所以用到new和delete的地方不一定非要使用_beginthreadex() 函数,用其他两个函数都可以。

4:_beginthreadex和_beginthread在回调入口函数之前进行一些线程相关的CRT的初始化操作。CRT的函数库在线程出现之前就已经存在,所以原有的CRT不能真正支持线程,这也导致了许多CRT的函数在多线程的情况下必须有特殊的支持,不能简单的使CreateThread就可以。

5:如果要作多线程(非MFC)程序,在主线程以外的任何线程内

使用malloc(),free(),new
调用stdio.h或io.h,包括fopen(),open(),getchar(),write(),printf(),errno
使用浮点变量和浮点运算函数
调用那些使用静态缓冲区的函数如: asctime(),strtok(),rand()等。

应该使用多线程的CRT并配合_beginthreadex(该函数只存在于多线程CRT), 其他情况你,可以使用单线程的CRT并配合CreateThread。因为对产生的线程而言,_beginthreadex比CreateThread会为上述操作多做额外的工作,比如帮助strtok()为每个线程准备一份缓冲区。
然而多线程程序极少情况不使用上述那些函数(比如内存分配或者io),所以与其每次都要思考是要使用_beginthreadex还是CreateThread,不如就一棍子敲定_beginthreadex。

6:你也许会借助win32来处理内存分配和io,这时候你确实可以以单线程crt配合CreateThread,因为io的重任已经从crt转交给了win32。这时通常你应该使用HeapAlloc,HeapFree来处理内存分配,用CreateFile或者GetStdHandle来处理io。

7:还有一点比较重要的是_beginthreadex传回的虽然是个unsigned long,其实是个线程Handle(事实上_beginthreadex在内部就是调用了CreateThread),所以你应该用CloseHandle来结束他。千万不要使用ExitThread()来退出_beginthreadex创建的线程,那样会丧失释放簿记数据的机会,应该使用_endthreadex.

下面对两个概念进行阐述

CRT(C/C++ Runtime Library):

是一种函数库,由编译器的生产厂家提供头文件或接口,操作系统提供运行时库的实现。所以Windows和Linux系统的运行时库函数接口虽然一样,但具体实现不一样。

CRT是支持C/C++运行的一系列函数和代码的总称,虽然没有一个很精确的定义,但是可以知道,你的main函数就是它负责调用的,还有平时使用的strlen,strtok,time,atoi之类的函数也是它提供的。

线程局部存储(TLS,thread local storage)

一个多线程程序中,全局变量(及分配的内存)被所有线程所共享。函数的静态局部变量也被所有使用该函数的线程所共享。一个函数中的自动变量对每一个线程是唯一的,因为它们存储于堆栈上,而每个线程都有他们自己的堆栈。有时,我们需要对每一个线程唯一的持续性存储。例如,C函数strtok就需要这种存储。不幸的是,C语言不支持这种变量。但是Windows提供了四个API函数来实现这种机制。我们把这种存储称为线程局部存储(TLS,Thread Local Storage)。

首先,定义一个结构,把对每个线程唯一的数据包含在该结构中。

例如:

typedef struct

{        int one;        int two;

} DATA, *PDATA;

然后,主线程调用TlsAlloc函数来为进程获得一个TLS索引:tlsIndex = TlsAlloc();该TLS索引可以存储于一个全局变量或者通过线程函数的参数传递给其它线程。每个需要使用该TLS索引的线程,先动态分配内存,然后调用TlsSetValue函数将该内存关联到该TLS索引(及该线程): TlsSetValue(tlsIndex, GlobalAlloc(GPTR, sizeof(DATA));

此时,线程直接或间接调用的函数可以通过如下方式获得该线程的TLS存储区域:
        PDATA pdata;
        pdata = (PDATA) TlsGetValue(tlsIndex);

此时,就可以使用该线程的TLS存储区的变量了。

当线程函数终止时,它应该释放它所分配的动态空间: GlobalFree(TlsGetValue(tlsIndex));

当所有使用TLS的线程都终止后,主线程应当释放该TLS存储空间: TlsFree(tlsIndex);

TLS可以以一种更简单的方式使用,那就是通过Winodws对C所作的扩展关键字__declspec和扩展存储类型修饰符thread。例如:
    __declspec(thread) int global_tls_i = 1;        // 在函数外部,声明一个TLS变量
    __declspec(thread) static int local_tls_i = 2;  // 在函数内部声明一个静态TLS变量