当多个线程无限制的在同一段时间内访问同一资源时,有可能导致错误的结果的发生,例:
#include <windows.h> #include <stdio.h> long g_iNum1,g_iNum2; DWORD WINAPI SubThread(LPVOID lpParam) { for(int i=0; i<10000; i++) { g_iNum1+=1; g_iNum2+=2; } return 0; } void main() { HANDLE hThreads[2]; g_iNum1=0; g_iNum2=0; hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL); hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL); SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST); SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST); //等待两个线程都执行结束 WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); printf("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }两个全局变量被两个线程竞争使用,而且没有保护。因此,最终输出的结果很有可能不是20000和40000。(如果 i 的上限设太小,一个线程分配的时间片就可以计算完成,就不会有竞争,结果也跟预期一样)
常用的同步处理机制包括互锁函数、临界区、互斥变量、信号量、事件等Windows内核对象。
1. 原子访问,互锁函数
互锁函数提供了一套多个线程同步访问一个简单变量的处理机制。
LONG InterlockedIncrement(LONG volatile* lpAddend);
该函数提供多线程情况下,对一个变量以原子操作方式增加1
LONG InterlockedDecrement(LONG volatile* lpAddend);
该函数提供多线程情况下,对一个变量以原子操作方式减少1
LONG InterlockedExchange(LONG volatile* lpTarget,LONG lValue);
该函数提供在多线程情况下,以原子操作方式用lValue给lpTarget指向的目标变量赋值,并返回赋值以前的lpTarget指向的值。
LONG InterlockedExchangeAdd(LONG volatile* lpAddend,LONG lValue)
该函数提供在多线程情况下,以院子的操作方式将lpAddend指向的变量增加lValue,并返回调用前的lpAddend指向的目标变量的值。
示例:
long g_iNum1,g_iNum2; DWORD WINAPI SubThread(LPVOID lpParam) { for(int i=0; i<10000; i++) { InterlockedIncrement(&g_iNum1); InterlockedExchangeAdd(&g_iNum2,2); } return 0; } void Test() { HANDLE hThreads[2]; g_iNum1=0; g_iNum2=0; hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL); hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL); SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST); SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST); WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
2. 临界区
临界区是一段连续的代码区域,它要求在执行前获得对某些共享数据的独占的访问权。
如果一个进程中的所有线程中访问这些共享数据的代码都放在临界区中,就能够实现对该共享数据的同步访问。临界区只能用于同步单个进程中的线程。
例://多个线程共享的全局数据 long g_iNum1,g_iNum2; //实例化临界区对象 CRITICAL_SECTION g_sec; DWORD WINAPI SubThread(LPVOID lpParam) { for(int i=0; i<10000; i++) { //进入临界区,临界区对象的引用计数加1,同一个线程可以多次调用 //EnterCriticalSection,但是如果调用n次EnterCriticalSection以后, //必须再调用n次的LeaveCriticalSection,使临界区对象的引用计数变为0, //其它的线程才能进入临界区 EnterCriticalSection(&g_sec); g_iNum1++; g_iNum2+=2; //离开临界区 LeaveCriticalSection(&g_sec); } return 0; } void Test() { HANDLE hThreads[2]; g_iNum1=0; g_iNum2=0; //初始化临界区对象 InitializeCriticalSection(&g_sec); hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL); hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL); SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST); SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST); WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); //释放临界区对象 DeleteCriticalSection(&g_sec); TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
3. 内核对象(用的比较多)
临界区非常适合于在同一个进程内部以序列化的方式访问共享的数据。然而,有时用户希望一个线程与其他线程执行的某些操作取得同步,这就需要使用内核对象来同步线程。
常用的内核对象有:互斥变量、信号量和事件,其他的还包括文件、控制台输入、文件变化通知、可等待的计时器。
每一个内核对象在任何时候都处于两种状态之一:信号态(signaled)和无信号态(nonsignaled)。
线程在等待其中的一个或多个内核对象时,如果在等待的一个或多个内核对象处于无信号态,线程自身将被系统挂起,直到等待的内核对象变为有信号状态时,线程才恢复运行。
常用的等待函数有2个:
1> 等待单个内核对象
DWORD WaitForSingleObject(
HANDLE hHandle, //指向内核对象的句柄
DWORD dwMilliseconds //等待的毫秒数,如果传入INFINITE,则无限期等待。
);
返回值及含义:
WAIT_OBJECT_0 对象处于有信号状态;
WAIT_TIMEOUT 对象在指定时间内没有变为有信号状态;
WAIT_ABANDONED 对象是一个互斥量,由于被放弃了而变为有信号状态;
WAIT_FAILED 发生了错误。调用GetLastError可以得到详细的错误信息;
2> 等待多个对象
DWORD WaitForMultipleObjects(
DWORD nCount, //对象的个数)
(1) 互斥变量
互斥量类似于临界区,但它能够同步多个进程间的数据访问。示例:
#include <windows.h> #include <stdio.h> long g_iNum1,g_iNum2; //同步对象 HANDLE g_hMutex=CreateMutex(NULL, FALSE, NULL); DWORD WINAPI SubThread(LPVOID lpParam) { for(int i=0; i<1000000; i++) { //等待互斥量,如果互斥量处于信号态,该函数返回,同时 //将g_hMutex变为无信号态 WaitForSingleObject(g_hMutex,INFINITE); g_iNum1++; g_iNum2+=2; //是互斥量重新处于信号态 ReleaseMutex(g_hMutex); } return 0; } void main() { HANDLE hThreads[2]; g_iNum1=0; g_iNum2=0; //创建互斥量 g_hMutex=CreateMutex(NULL,FALSE,"MutexForSubThread"); hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL); hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL); WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); //关闭互斥量 CloseHandle(g_hMutex); printf("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }测试感觉速度很慢,不知道是不是内核态和用户态切换的原因;
另外,hMutex=NULL; 的时候都可以运行...
在上例中,主线程创建互斥量,并在创建的同时使互斥量处于信号态。两个子线程在运行过程中,通过调用WaitForSingleObject等待该互斥量,如果此时互斥量处于无信号态,则在等待线程被系统挂起,一直等到互斥量变为信号态才继续运行,WaitForSingleObject返回的同时,将使互斥量再次变为无信号态。最后线程通过调用ReleaseMutex使互斥量变为信号态。
(2) 信号量
信号量内核对象用于资源计数。每当线程调用WaitForSingleObjec()函数并传入一个信号量对象的句柄,系统将检查该信号量的资源计数是否大于0;
如果大于0,表示有资源可用,此时系统就将资源计数减去1,并唤醒线程;
如果等于0,表示无资源可用,系统就将线程挂起,直到另外一个线程释放了该对象,释放信号量意味着增加它的资源计数。
信号量与临界区和互斥量不同,信号量不属于任何线程。因此可以在一个线程中增加信号量的计数,而在另一个线程中减少信号量的计数。
但是在使用过程中,信号量的使用与互斥量非常相似,互斥量可以看作是信号量的一个特殊版本,即可以将互斥量看作最大资源计数为1的信号量。
通过调用CreateSemaphore()函数可以创建一个信号量:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes;);
其中:
lMaximumCount指定信号量的最大计数;
lInitialCount指定信号量的初始计数;
lpName指定对象的名称;
其他进程中的线程使用该名称调用CreateSemaphore()函数或OpenSemaphore()函数得到信号量的句柄:
HANDLE OpenSemaphore();
线程使用ReleaseSemaphore()函数释放信号量:
BOOL ReleaseSemaphore();
该函数可以一次增加信号量的大于1的计数,参数lReleaseCount指出一次增加的量。函数在参数lpPreviousCount中返回该函数调用前的信号量的计数。
信号量常用在如下情况下。M个线程对N个共享资源的访问,其中M>N
下面示例模拟32个线程对4个数据库连接对象的访问:
//数据库连接对象的包装 class CMyConnection { private: BOOL m_bUse; public: LPVOID m_pObj; //模拟连接对象 friend class CMyConnectionPool; }; //数据库缓冲池 class CMyConnectionPool { protected: CMyConnection *m_pConn; // int m_iCount; HANDLE m_hSemaphore; //信号量 public: CMyConnectionPool(int iCount) { m_iCount=iCount; m_pConn=new CMyConnection[iCount]; for(int i=0; i<m_iCount; i++) { m_pConn[i].m_bUse=FALSE; //m_pConn[i].m_pObj=...模拟连接对象的初始化 m_hSemaphore=CreateSemaphore(NULL,iCount,iCount, _T("SemaphoreForMyConnectionPool")); } } ~CMyConnectionPool() { delete []m_pConn; CloseHandle(m_hSemaphore); } CMyConnection *GetConnection() { WaitForSingleObject(m_hSemaphore,INFINITE); for(int i=0; i<m_iCount; i++) { if(m_pConn[i].m_bUse==FALSE) { m_pConn[i].m_bUse=TRUE; return m_pConn+i; } } ASSERT(FALSE);//应该永远都不会执行到这里 return NULL; } void ReleaseConnection(CMyConnection *pConn) { pConn->m_bUse=FALSE; ReleaseSemaphore(m_hSemaphore,1,NULL); } }; //实例化4个数据库连接对象,并放进缓冲池中使用。 CMyConnectionPool g_pool(4); DWORD WINAPI SubThread(LPVOID lpParam) { CMyConnection *pConn=g_pool.GetConnection(); static long lSubThreadCount=0; InterlockedIncrement(&lSubThreadCount); TRACE(_T("当前线程ID:%X, 子线程数:%d\n"), GetCurrentThreadId(), lSubThreadCount); Sleep(2000);//模拟数据库的访问 InterlockedDecrement(&lSubThreadCount); g_pool.ReleaseConnection(pConn); return 0; } void Test() { const int iThreadCount=32; HANDLE hThreads[iThreadCount]; for(int i=0; i<m_icount; i++) hThreads[i]=CreateThread(NULL,0,SubThread,NULL,0,NULL); WaitForMultipleObjects(iThreadCount,hThreads,TRUE,INFINITE); }
(3) 事件对象
与互斥量和信号量不同,互斥变量和信号量用于控制对共享数据的访问,而事件发送信号表示某一操作已经完成。
有两种事件对象:手动重置事件和自动重置事件。
手动重置事件用于同时向多个线程发送信号;
自动重置事件用于向一个线程发送信号。
如果有多个线程调用WaitForSingleObject()或者WaitForMultipleObjects()等待一个自动重置事件,那么当该自动重置事件变为信号态时,其中的一个线程会被唤醒,被唤醒的线程开始继续运行,同时自动重置事件又被置为无信号态,其他线程依旧处于挂起状态。从这一点看,自动重置事件有点类似于互斥量。
手动重置事件不会被WaitForSingleObject()和WaitForMultipleObjects()自动重置为无信号态,需要调用相应的函数才能将手动重置事件重置为无信号态。因此,当手工重置事件有信号时,所有等待该事件的线程都将被激活。
主要的几个函数:
1> 使用CreateEvent()函数创建事件对象:
HANDLE CreateEvent();
其中:
bManualReset为true时,指定创建的是手动重置事件,否则为自动重置事件;
bInitialState表示事件对象被初始化时是信号态还是无信号态;
参数lpName指定事件对象的名称,其他进程中的线程可以通过该名称调用CreateEvent()或者OpenEvent()函数得到该事件对象的句柄。
HANDLE OpenEvent();
2> 通过SetEvent()函数设置为信号态:
注意:无论自动重置事件对象还是手工重置事件对象,都可以设置为有信号状态!
);
3> 通过ResetEvent函数设置为无信号态:
注意:无论自动重置事件对象还是手工重置事件对象,都可以
);
不过对于自动重置事件不必执行ResetEvent,因为系统会在WaitForSingleObject()或者WaitForMultipleObjects()返回前,自动将事件对象置为无信号态。
4> 调用CloseHandle()函数关闭事件。
查看WINAPI函数调用失败原因的描述的辅助函数,供参考:
CString GetLastErrorStr() { CString sRet=_T(""); LPVOID lpMsgBuf; DWORD dwError=GetLastError(); if(dwError) { if(FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language (LPTSTR) &lpMsgBuf, 0, NULL)) { sRet.Format(_T("%08X: %s"),dwError,lpMsgBuf); sRet.Replace(_T("\r\n"),_T("")); LocalFree(lpMsgBuf); } } return sRet; }