【多线程】多线程教程之二---线程间同步

来源:互联网 发布:淘宝哪家电脑主机好 编辑:程序博客网 时间:2024/06/16 11:22

数据竞争:如果有两个或者是两个以上的任务同时更改共享的数据,而数据的最终值取决于那个任务先到达,就出现了竞争条件,当两个或多个任务试图同时更新相同的数据资源时,竞争条件被称为 数据竞争。


如果多个线程或进程师徒同时访问不能修改的资源(如只读内存或常量),则不用担心数据竞争,与此类似,若多个线程或进程只是试图读取一个数据块,也不会发生数据竞争!


竞争条件出现的前提是

(1)资源是可修改的

(2)多个进程或线程同时访问

(3)至少有一个线程或进程试图修改资源


当多个线程无限制的在同一段时间内访问同一资源时,有可能导致错误的结果的发生,例:

  1. #include <windows.h>  
  2. #include <stdio.h>  
  3.   
  4. long g_iNum1,g_iNum2;  
  5.   
  6. DWORD WINAPI SubThread(LPVOID lpParam)  
  7. {  
  8.     for(int i=0; i<10000; i++)  
  9.     {  
  10.         g_iNum1+=1;  
  11.         g_iNum2+=2;  
  12.     }  
  13.     return 0;  
  14. }  
  15.   
  16. void main()  
  17. {  
  18.     HANDLE hThreads[2];  
  19.     g_iNum1=0;  
  20.     g_iNum2=0;  
  21.     hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  22.     hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  23.     SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);  
  24.     SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);  
  25.     //等待两个线程都执行结束,没有这一句,输出的都是0;  
  26.     WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);  
  27.     printf("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2);  
  28. }

两个全局变量被两个线程竞争使用,而且没有保护。因此,最终输出的结果很有可能不是20000和40000。(如果 i 的上限设太小,一个线程分配的时间片就可以计算完成,就不会有竞争,结果也跟预期一样)

常用的同步处理机制包括互锁函数、临界区、互斥变量、信号量、事件等Windows内核对象。

1.锁

      大多数情况下不会通过将普通变量变为同步变量来避免数据竞争,而是通过防止对普通变量的并发访问来避免数据竞争。这通常通过引入锁(有时称为互斥锁)来实现,锁是编程语言或支持库提供的特殊同步变量。例如,如果我们想维护一个共享计数器变量c,它的值可以被多个线程递增,并在线程结束时读取其值。我们可以引入一个对应的锁l,编写如下代码:

void increment_c()
2{
3    l.lock();
4    ++c;
5    l.unlock();
6}
    lock()和unlock()的实现确保了在这两个调用之间同时至多只能有一个线程,所以同时只有一个线程能访问c。我们可以将这看做对交错执行进行了限制:对于给定的锁l,l.lock()和l.unlock()交替调用,并且初始调用是l.lock()。(一般情况下还有一个额外的需求:锁必须由获取它的那个线程释放,但不总是这样。因此即便increment_c()被多个线程并发调用,在c也上是没有数据竞争的。交错执行中任何两个对c的访问都会至少被第一个线程中的unlock()和第二个线程中的lock()分开。

(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指向的目标变量的值。

示例:

  1. long g_iNum1,g_iNum2;  
  2.  
  3. DWORD WINAPI SubThread(LPVOID lpParam)  
  4. {  
  5.     for(int i=0; i<10000; i++)  
  6.     {  
  7.         InterlockedIncrement(&g_iNum1);  
  8.         InterlockedExchangeAdd(&g_iNum2,2);  
  9.     }  
  10.     return 0;  
  11. }  

  12. void Test()  
  13. {  
  14.     HANDLE hThreads[2];  
  15.     g_iNum1=0;  
  16.     g_iNum2=0;  
  17.     hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  18.     hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  19.     SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);  
  20.     SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);  
  21.    //等待线程结束
  22.     WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);  
  23.     TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2);  
  24. }  

2. 临界区

临界区是一段连续的代码区域,它要求在执行前获得对某些共享数据的独占的访问权。

如果一个进程中的所有线程中访问这些共享数据的代码都放在临界区中,就能够实现对该共享数据的同步访问。临界区只能用于同步单个进程中的线程。

  1. //多个线程共享的全局数据  
  2. long g_iNum1,g_iNum2;  
  3. //实例化临界区对象  
  4. CRITICAL_SECTION g_sec;  
  5. DWORD WINAPI SubThread(LPVOID lpParam)  
  6. {  
  7.     for(int i=0; i<10000; i++)  
  8.     {  
  9.         //进入临界区,临界区对象的引用计数加1,同一个线程可以多次调用  
  10.         //EnterCriticalSection,但是如果调用n次EnterCriticalSection以后,  
  11.         //必须再调用n次的LeaveCriticalSection,使临界区对象的引用计数变为0,  
  12.         //其它的线程才能进入临界区  
  13.         EnterCriticalSection(&g_sec);  
  14.         g_iNum1++;  
  15.         g_iNum2+=2;  
  16.         //离开临界区  
  17.         LeaveCriticalSection(&g_sec);  
  18.     }  
  19.     return 0;  
  20.     }  
  21.    
  22. void Test()   
  23. {  
  24.     HANDLE hThreads[2];  
  25.     g_iNum1=0;  
  26.     g_iNum2=0;  
  27.     //初始化临界区对象  
  28.     InitializeCriticalSection(&g_sec);  
  29.     hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  30.     hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  31.     SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);  
  32.     SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);  
  33.     WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);  
  34.     //释放临界区对象  
  35.     DeleteCriticalSection(&g_sec);  
  36.     TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2);  
  37. }  

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,                                   //对象的个数
CONST HANDLE *lpHandles,            //对象句柄数组
BOOL bWaitAll,                                      //是否要等到所有的对象都变为信号态
DWORD dwMilliseconds                     //等待的毫秒数,如果传入INFINITE,则无限期等待。

)

(1) 互斥变量

互斥量类似于临界区,但它能够同步多个进程间的数据访问

示例:

[cpp] view plain copy
  1. #include <windows.h>  
  2. #include <stdio.h>  
  3.   
  4. long g_iNum1,g_iNum2;  
  5. //同步对象  
  6. HANDLE g_hMutex=CreateMutex(NULL, FALSE, NULL);  
  7.    
  8. DWORD WINAPI SubThread(LPVOID lpParam)  
  9. {  
  10.     for(int i=0; i<1000000; i++)  
  11.     {  
  12.         //等待互斥量,如果互斥量处于信号态,该函数返回,同时  
  13.         //将g_hMutex变为无信号态  
  14.         WaitForSingleObject(g_hMutex,INFINITE);  
  15.         g_iNum1++;  
  16.         g_iNum2+=2;  
  17.         //是互斥量重新处于信号态  
  18.         ReleaseMutex(g_hMutex);  
  19.     }  
  20.     return 0;  
  21. }  
  22.   
  23. void main()   
  24. {  
  25.     HANDLE hThreads[2];  
  26.     g_iNum1=0;  
  27.     g_iNum2=0;  
  28.     //创建互斥量  
  29.     g_hMutex=CreateMutex(NULL,FALSE,"MutexForSubThread");  
  30.     hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  31.     hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  32.     WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);  
  33.     //关闭互斥量  
  34.     CloseHandle(g_hMutex);  
  35.     printf("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2);  
  36. }  
测试感觉速度很慢,不知道是不是内核态和用户态切换的原因;

另外,hMutex=NULL; 的时候都可以运行...

在上例中,主线程创建互斥量,并在创建的同时使互斥量处于信号态。两个子线程在运行过程中,通过调用WaitForSingleObject等待该互斥量,如果此时互斥量处于无信号态,则在等待线程被系统挂起,一直等到互斥量变为信号态才继续运行,WaitForSingleObject返回的同时,将使互斥量再次变为无信号态。最后线程通过调用ReleaseMutex使互斥量变为信号态。

(2) 信号量

信号量内核对象用于资源计数。每当线程调用WaitForSingleObjec()函数并传入一个信号量对象的句柄,系统将检查该信号量的资源计数是否大于0;

如果大于0,表示有资源可用,此时系统就将资源计数减去1,并唤醒线程;

如果等于0,表示无资源可用,系统就将线程挂起,直到另外一个线程释放了该对象,释放信号量意味着增加它的资源计数。

信号量与临界区和互斥量不同,信号量不属于任何线程。因此可以在一个线程中增加信号量的计数,而在另一个线程中减少信号量的计数。

但是在使用过程中,信号量的使用与互斥量非常相似,互斥量可以看作是信号量的一个特殊版本,即可以将互斥量看作最大资源计数为1的信号量。

通过调用CreateSemaphore()函数可以创建一个信号量:

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes;
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName

);

其中:

lMaximumCount指定信号量的最大计数;

lInitialCount指定信号量的初始计数;

lpName指定对象的名称;

其他进程中的线程使用该名称调用CreateSemaphore()函数或OpenSemaphore()函数得到信号量的句柄:

HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName

);

线程使用ReleaseSemaphore()函数释放信号量:

BOOL ReleaseSemaphore(
 HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount

);

该函数可以一次增加信号量的大于1的计数,参数lReleaseCount指出一次增加的量。函数在参数lpPreviousCount中返回该函数调用前的信号量的计数。

信号量常用在如下情况下。M个线程对N个共享资源的访问,其中M>N

下面示例模拟32个线程对4个数据库连接对象的访问:

[cpp] view plain copy
  1. //数据库连接对象的包装  
  2. class CMyConnection  
  3. {  
  4. private:  
  5. BOOL m_bUse;  
  6. public:  
  7. LPVOID m_pObj; //模拟连接对象  
  8. friend class CMyConnectionPool;  
  9. };  
  10. //数据库缓冲池  
  11. class CMyConnectionPool  
  12. {  
  13. protected:  
  14.     CMyConnection *m_pConn; //  
  15.     int m_iCount;  
  16.     HANDLE m_hSemaphore; //信号量  
  17. public:  
  18.     CMyConnectionPool(int iCount)  
  19.     {  
  20.         m_iCount=iCount;  
  21.         m_pConn=new CMyConnection[iCount];  
  22.         for(int i=0; i<m_iCount; i++)  
  23.         {  
  24.             m_pConn[i].m_bUse=FALSE;  
  25.             //m_pConn[i].m_pObj=...模拟连接对象的初始化  
  26.             m_hSemaphore=CreateSemaphore(NULL,iCount,iCount,  
  27.             _T("SemaphoreForMyConnectionPool"));  
  28.         }  
  29.     }  
  30.     ~CMyConnectionPool()  
  31.     {  
  32.         delete []m_pConn;  
  33.         CloseHandle(m_hSemaphore);  
  34.     }  
  35.    
  36.     CMyConnection *GetConnection()  
  37.     {  
  38.         WaitForSingleObject(m_hSemaphore,INFINITE);  
  39.         for(int i=0; i<m_iCount; i++)  
  40.         {  
  41.             if(m_pConn[i].m_bUse==FALSE)  
  42.             {  
  43.                 m_pConn[i].m_bUse=TRUE;  
  44.                 return m_pConn+i;  
  45.             }  
  46.         }  
  47.         ASSERT(FALSE);//应该永远都不会执行到这里  
  48.         return NULL;  
  49.     }  
  50.    
  51.     void ReleaseConnection(CMyConnection *pConn)  
  52.     {  
  53.         pConn->m_bUse=FALSE;  
  54.         ReleaseSemaphore(m_hSemaphore,1,NULL);  
  55.     }  
  56. };  
  57.    
  58. //实例化4个数据库连接对象,并放进缓冲池中使用。  
  59. CMyConnectionPool g_pool(4);  
  60.    
  61. DWORD WINAPI SubThread(LPVOID lpParam)  
  62. {  
  63.     CMyConnection *pConn=g_pool.GetConnection();  
  64.     static long lSubThreadCount=0;  
  65.     InterlockedIncrement(&lSubThreadCount);  
  66.     TRACE(_T("当前线程ID:%X, 子线程数:%d\n"),  
  67.     GetCurrentThreadId(),  
  68.     lSubThreadCount);  
  69.     Sleep(2000);//模拟数据库的访问  
  70.     InterlockedDecrement(&lSubThreadCount);  
  71.     g_pool.ReleaseConnection(pConn);  
  72.     return 0;  
  73. }  
  74.    
  75. void Test()   
  76. {  
  77.     const int iThreadCount=32;  
  78.     HANDLE hThreads[iThreadCount];  
  79.     for(int i=0; i<m_icount; i++)  
  80.         hThreads[i]=CreateThread(NULL,0,SubThread,NULL,0,NULL);  
  81.     WaitForMultipleObjects(iThreadCount,hThreads,TRUE,INFINITE);  
  82. }  

(3) 事件对象

与互斥量和信号量不同,互斥变量和信号量用于控制对共享数据的访问,而事件发送信号表示某一操作已经完成。

有两种事件对象:手动重置事件和自动重置事件。

手动重置事件用于同时向多个线程发送信号

自动重置事件用于向一个线程发送信号

如果有多个线程调用WaitForSingleObject()或者WaitForMultipleObjects()等待一个自动重置事件,那么当该自动重置事件变为信号态时,其中的一个线程会被唤醒,被唤醒的线程开始继续运行,同时自动重置事件又被置为无信号态,其他线程依旧处于挂起状态。从这一点看,自动重置事件有点类似于互斥量。

手动重置事件不会被WaitForSingleObject()和WaitForMultipleObjects()自动重置为无信号态,需要调用相应的函数才能将手动重置事件重置为无信号态。因此,当手工重置事件有信号时,所有等待该事件的线程都将被激活。

主要的几个函数:

1> 使用CreateEvent()函数创建事件对象

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName

);

其中:

bManualReset为true时,指定创建的是手动重置事件,否则为自动重置事件;

bInitialState表示事件对象被初始化时是信号态还是无信号态;

参数lpName指定事件对象的名称,其他进程中的线程可以通过该名称调用CreateEvent()或者OpenEvent()函数得到该事件对象的句柄。

HANDLE OpenEvent(
 DWORD dwDesiredAccess,
 BOOL bInheritHandle,
 LPCTSTR lpName

);

2> 通过SetEvent()函数设置为信号态

注意:无论自动重置事件对象还是手工重置事件对象,都可以设置为有信号状态!

BOOL SetEvent(
HANDLE hEvent

);

3> 通过ResetEvent函数设置为无信号态

注意:无论自动重置事件对象还是手工重置事件对象,都可以

BOOL ResetEvent(
HANDLE hEvent

);

不过对于自动重置事件不必执行ResetEvent,因为系统会在WaitForSingleObject()或者WaitForMultipleObjects()返回前,自动将事件对象置为无信号态。

4> 调用CloseHandle()函数关闭事件。

查看WINAPI函数调用失败原因的描述的辅助函数,供参考:

[cpp] view plain copy
  1. CString GetLastErrorStr()  
  2. {  
  3.     CString sRet=_T("");  
  4.     LPVOID lpMsgBuf;  
  5.     DWORD dwError=GetLastError();  
  6.     if(dwError)  
  7.     {  
  8.         if(FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |   
  9.                 FORMAT_MESSAGE_FROM_SYSTEM |  
  10.                 FORMAT_MESSAGE_IGNORE_INSERTS,  
  11.                 NULL,  
  12.                 dwError,  
  13.                 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language  
  14.                 (LPTSTR) &lpMsgBuf,  
  15.                 0,  
  16.             NULL))  
  17.         {  
  18.             sRet.Format(_T("%08X: %s"),dwError,lpMsgBuf);  
  19.       sRet.Replace(_T("\r\n"),_T(""));  
  20.         LocalFree(lpMsgBuf);  
  21.         }  
  22.     }  
  23.     return sRet;  
  24. }  

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 qq钱包被冻结了微信怎么办 q币不小心充多了怎么办 u盘装系统就一个图标怎么办 淘宝买了东西退货客服不理人怎么办 微信10w限额满了怎么办 微信身份证实名认证超出限额怎么办 微信信用卡消费超过当日限额怎么办 淘宝客服同意退货卖家拒绝怎么办 微信钱包充值话费不到帐怎么办 京东买的显示器过保坏了怎么办 支付宝充话费充错号码是空号怎么办 京东充话费充错了号码该怎么办 微信红包充话费不到账怎么办 支付宝充话费等待第三方发货怎么办 微信充话费显示成功但没收到怎么办 微信退款一直在退款中怎么办 文件大于100发不了微信怎么办 微信的传送文件大于100怎么办 微信钱包话费充值错误怎么办 微信转账到不了账也退不回是怎么办 求人办事微信发红包对方不收怎么办 微信上交了订金对方不退怎么办 交通事故对方伤员堵大门搂腿怎么办 电脑开机桌面文件都没了怎么办 qq飞车手游队长换了微信群怎么办 qq飞车手游登录授权失败怎么办 安装时提示安装包发现错误怎么办 苹果6p升级系统验证失败怎么办 w10开不了机无限重启怎么办 微信朋友圈里的表情图打不开怎么办 金立手机微信启动录音被拒绝怎么办 微信帐号解封后漂流瓶不能用怎么办 微信怎么在电脑上登不上去怎么办 玩旧版60级魔兽经常花屏怎么办? 我的世界手机版物品栏不见了怎么办 苹果手机掉进水里出现花屏该怎么办 球球大作战还没进去停止运行怎么办 ps3 e3硬破芯片坏了怎么办 电话打开后页面上没有东西怎么办 WPS在电脑安装后卸载不了怎么办 ps总要以管理员的身份打开怎么办