===============================================================================
CreateThread,_beginthread,_beginthreadx 的区别
===============================================================================
用使用 CreateThread 是 Windows 的 API 函数,只需要和 Kernel32.lib 库链接。
用使用 _beginthread 和 _beginthreadex,应用必须和 CRT(C RunTime) 库链接。
所以一个线程要使用静态 CRT(C RunTime)的库函数,必须使用 _beginthread 和 _beginthreadex 函数。
不过,在 _beginthread 和 _beginthreadex 函数的内部实习代码中调用的是 CreateThread 函数来实现的(这很显然嘛,CRT 库也是要运行在Windows上)。
具体 CreateThread 和 _beginthread/_beginthreadex 之间的区别:
直接在CreateThread API创建的线程中使用sprintf,malloc,strcat等涉及CRT存储堆操作的CRT库函数是很危险的,容易造成线程的意外中止。 在使用_beginthread和_beginthreadex创建的线程中可以安全的使用CRT函数,但是必须在线程结束的时候相应的调用_endthread或_endthreadex。
具体 _beginthread 和 _beginthreadex 之间的区别:
虽然都是内部调用CreateThread API创建线程,但是这两个函数还是有很大的区别。
首先,_beginthreadex函数比_beginthread函数多了三个参数,使它看起来更像CreateThread而不是同胞兄弟_beginthread;
其次,_beginthread要求线程函数是__cdecl调用约定,并且没有返回值,而_beginthreadex则要求线程函数是__stdcall调用约定并且返回线程的退出码;
第三,与 _beginthread 成对调用的 _endthread 函数内部隐式的调用 CloseHandle 关闭了线程句柄,而与_beginthreadex 成对使用的 _endthreadex 则没有关闭线程的句柄,需要显示的调用CloseHandle关闭线程句柄;
最后,这两个函数在执行成功时返回线程的句柄,可以通过这个句柄调用其它线程相关的API,比如SetThreadPriority,ResumeThread等等。但是执行失败时返回值是不同的,_beginthread 返回 -1 表示失败,而_beginthreadex返回 0 表示失败。
===============================================================================
扩展知识
===============================================================================
标准 C 运行库在 1970 年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准 C 运行库的程序员根本没考虑多线程程序使用标准 C 运行库的情况。
比如标准 C 运行库的全局变量 errno。很多运行库中的函数在出错时会将错误代号赋值给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:
if (system("notepad.exe readme.txt") == -1)
{
switch(errno)
{
...//错误处理代码
}
}
假设某个线程 A 在执行上面的代码,该线程在调用 system() 之后且尚未调用 switch() 语句时另外一个线程 B 启动了,这个线程 B 也调用了标准 C 运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量 errno 中。这样线程 A 一旦开始执行 switch() 语句时,它将访问一个被 B 线程改动了的 errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime() 等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。
为了解决这个问题,Windows 操作系统提供了这样的一种解决方案--每个线程都将拥有自己专用的一块内存区域来供标准 C 运行库中所有有需要的函数使用。而且这块内存区域的创建就是由 C/C++ 运行库函数 _beginthreadex()来负责的。
下面列出 _beginthreadex() 函数的源代码(我在这份代码中增加了一些注释)以便读者更好的理解 _beginthreadex() 函数与 CreateThread() 函数的区别。
_MCRTIMP uintptr_t __cdecl _beginthreadex(
void *security,
unsigned stacksize,
unsigned (__CLR_OR_STD_CALL * initialcode) (void *),
void * argument,
unsigned createflag,
unsigned *thrdaddr
)
{
_ptiddata ptd; //pointer to per-thread data 见注1
uintptr_t thdl; //thread handle 线程句柄
unsigned long err = 0L; //Return from GetLastError()
unsigned dummyid; //dummy returned thread ID 线程ID号
// validation section 检查initialcode是否为NULL
_VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);
//Initialize FlsGetValue function pointer
__set_flsgetvalue();
//Allocate and initialize a per-thread data structure for the to-be-created thread.
//相当于new一个_tiddata结构,并赋给_ptiddata指针。
if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
goto error_return;
// Initialize the per-thread data
//初始化线程的_tiddata块即CRT数据区域 见注2
_initptd(ptd, _getptd()->ptlocinfo);
//设置_tiddata结构中的其它数据,这样这块_tiddata块就与线程联系在一起了。
ptd->_initaddr = (void *) initialcode; //线程函数地址
ptd->_initarg = argument; //传入的线程参数
ptd->_thandle = (uintptr_t)(-1);
#if defined (_M_CEE) || defined (MRTDLL)
if(!_getdomain(&(ptd->__initDomain))) //见注3
{
goto error_return;
}
#endif // defined (_M_CEE) || defined (MRTDLL)
// Make sure non-NULL thrdaddr is passed to CreateThread
if ( thrdaddr == NULL )//判断是否需要返回线程ID号
thrdaddr = &dummyid;
// Create the new thread using the parameters supplied by the caller.
//_beginthreadex()最终还是会调用CreateThread()来向系统申请创建线程
if ( (thdl = (uintptr_t)CreateThread(
(LPSECURITY_ATTRIBUTES)security,
stacksize,
_threadstartex,
(LPVOID)ptd,
createflag,
(LPDWORD)thrdaddr))
== (uintptr_t)0 )
{
err = GetLastError();
goto error_return;
}
//Good return
return(thdl); //线程创建成功,返回新线程的句柄.
//Error return
error_return:
//Either ptd is NULL, or it points to the no-longer-necessary block
//calloc-ed for the _tiddata struct which should now be freed up.
//回收由_calloc_crt()申请的_tiddata块
_free_crt(ptd);
// Map the error, if necessary.
// Note: this routine returns 0 for failure, just like the Win32
// API CreateThread, but _beginthread() returns -1 for failure.
//校正错误代号(可以调用GetLastError()得到错误代号)
if ( err != 0L )
_dosmaperr(err);
return( (uintptr_t)0 ); //返回值为NULL的效句柄
}
讲解下部分代码:
注1._ptiddata ptd; 中的 _ptiddata 是个结构体指针。在 mtdll.h 文件被定义:
typedef struct _tiddata * _ptiddata
微软对它的注释为 Structure for each thread's data。这是一个非常大的结构体,有很多成员。本文由于篇幅所限就不列出来了。
注2.
_initptd(ptd, _getptd()->ptlocinfo);
微软对这一句代码中的getptd()的说明为:
_ptiddata __cdecl _getptd(void);
对_initptd()说明如下:
void __cdecl _initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);
注释中的CRT (C Runtime Library)即标准C运行库。
注3.
if(!_getdomain(&(ptd->__initDomain)))
中的_getdomain()函数代码可以在thread.c文件中找到,其主要功能是初始化COM环境。
由上面的源代码可知,_beginthreadex() 函数在创建新线程时会分配并初始化一个 _tiddata 块。这个 _tiddata 块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将 _tiddata 块与自己进一步关联起来。然后新线程调用标准 C 运行库函数如 strtok() 时就会先取得 _tiddata 块的地址再将需要保护的数据存入 _tiddata 块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。
因此,如果在代码中有使用标准 C 运行库中的函数时,尽量使用 _beginthreadex() 来代替 CreateThread()。
==============================================================================
扩展知识
==============================================================================
--------------------------------------------------------------------------------
下面列出一些常用的Interlocked系列函数:
--------------------------------------------------------------------------------
1.增减操作
LONG__cdeclInterlockedIncrement(LONG volatile* Addend);
LONG__cdeclInterlockedDecrement(LONG volatile* Addend);
返回变量执行增减操作之后的值。
LONG__cdec InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);
返回运算后的值,注意!加个负数就是减。
2.赋值操作
LONG__cdeclInterlockedExchange(LONG volatile* Target, LONGValue);
Value就是新值,函数会返回原先的值。
--------------------------------------------------------------------------------
关键段CRITICAL_SECTION
--------------------------------------------------------------------------------
函数功能:初始化
函数原型:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:定义关键段变量后必须先初始化。
函数功能:销毁
函数原型:
void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:用完之后记得销毁。
函数功能:进入关键区域
函数原型:
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:系统保证各线程互斥的进入关键区域。
函数功能:离开关关键区域
函数原型:
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
RTL_CRITICAL_SECTION在WinNT.h中声明,它其实是个结构体:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUGDebugInfo;
LONGLockCount;
LONGRecursionCount;
HANDLEOwningThread; // from the thread's ClientId->UniqueThread
HANDLELockSemaphore;
DWORDSpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
各个参数的解释如下:
第一个参数:PRTL_CRITICAL_SECTION_DEBUGDebugInfo;调试用的。
第二个参数:LONGLockCount;初始化为-1,n表示有n个线程在等待。
第三个参数:LONGRecursionCount; 表示该关键段的拥有线程对此资源获得关键段次数,初为0。
第四个参数:HANDLEOwningThread; 即拥有该关键段的线程句柄,微软对其注释为--from the thread's ClientId->UniqueThread
第五个参数:HANDLELockSemaphore;实际上是一个自复位事件。
第六个参数:DWORDSpinCount; 旋转锁的设置,单CPU下忽略
由于将线程切换到等待状态的开销较大,因此为了提高关键段的性能,Microsoft将旋转锁合并到关键段中,这样EnterCriticalSection() 会先用一个旋转锁不断循环,尝试一段时间才会将线程切换到等待状态。值得注意的是如果主机只有一个处理器,那么设置旋转锁是无效的。无法进入关键区域的线程总会被系统将其切换到等待状态。
下面是配合了旋转锁的关键段初始化函数:
函数功能:初始化关键段并设置旋转次数
函数原型:
BOOLInitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTIONlpCriticalSection,DWORDdwSpinCount);
函数说明:旋转次数一般设置为4000。
函数功能:修改关键段的旋转次数
函数原型:
DWORDSetCriticalSectionSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
--------------------------------------------------------------------------------
事件 event
--------------------------------------------------------------------------------
第一个 CreateEvent
函数功能:创建事件
函数原型:
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,
BOOLbManualReset,
BOOLbInitialState,
LPCTSTRlpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。
第三个参数表示事件的初始状态,传入TRUR表示已触发。
第四个参数表示事件的名称,传入NULL表示匿名事件。
第二个 OpenEvent
函数功能:根据名称获得一个事件句柄。
函数原型:
HANDLEOpenEvent(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName //名称
);
函数说明:
第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示事件句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。
第三个SetEvent
函数功能:触发事件
函数原型:
BOOLSetEvent(HANDLEhEvent);
函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。
第四个ResetEvent
函数功能:将事件设为末触发
函数原型:
BOOLResetEvent(HANDLEhEvent);
最后一个事件的清理与销毁
由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
第五个PulseEvent
函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。
函数原型:
BOOLPulseEvent(HANDLEhEvent);
函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:
1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。
2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。
此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。
--------------------------------------------------------------------------------
互斥量Mutex
--------------------------------------------------------------------------------
第一个 CreateMutex
函数功能:创建互斥量(注意与事件Event的创建函数对比)
函数原型:
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第二个打开互斥量
函数原型:
HANDLEOpenMutex(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName //名称
);
函数说明:
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第三个触发互斥量
函数原型:
BOOLReleaseMutex (HANDLEhMutex)
函数说明:
访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。
最后一个清理互斥量
由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。
互斥量也是有“线程拥有权”概念的。“线程拥有权”在关键段中有详细的说明,这里就不再赘述了。另外由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性--“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。
--------------------------------------------------------------------------------
信号量Semaphore
--------------------------------------------------------------------------------
第一个 CreateSemaphore
函数功能:创建信号量
函数原型:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数表示初始资源数量。
第三个参数表示最大并发数量。
第四个参数表示信号量的名称,传入NULL表示匿名信号量。
第二个 OpenSemaphore
函数功能:打开信号量
函数原型:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
函数说明:
第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示信号量句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。
第三个 ReleaseSemaphore
函数功能:递增信号量的当前资源计数
函数原型:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
函数说明:
第一个参数是信号量的句柄。
第二个参数表示增加个数,必须大于0且不超过最大资源数量。
第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。
注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
最后一个 信号量的清理与销毁
由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
注: 拥有线程拥有权限的互斥类型,在拥有的线程中可以重复进入。