前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。
1.临界区criticalSection
1对变量的操作上锁,别的地方就不能操作。
2 对函数上锁,别的地方就不能执行。
临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它们对应的函数分别为WriteThread()和ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:
#include "afxmt.h"
int array[10],destarray[10];
CCriticalSection Section;
////////////////////////////////////////////////////////////////////////
UINT WriteThread(LPVOID param)
{Section.Lock();
for(int x=0;x<10;x++)
array[x]=x;
Section.Unlock();
}
UINT ReadThread(LPVOID param)
{
Section.Lock();
For(int x=0;x<10;x++)
Destarray[x]=array[x];
Section.Unlock();
}
上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如果不使用同步,则不是这个结果,有兴趣的读者可以实验一下。
例:临界区模拟一个推模式,伪代码如下:
//临界区模拟一个推模式。
critsection crits;
bool bHaveData = false;
sharesection section;
写:
crits.lock();
if(!bHaveData)
{
section.write();
bHaveData = true;
}
crits.unlock();
读:
bool bNeedDeal = false;
crits.lock();
if(bHaveData)
{
bNeedDeal = true;
}
crits.unlock();
if(bNeedDeal)
{
section.read();
crits.lock();
bHaveData = false;
crits.unlock();
}
2.互斥mutex
互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互斥与Cmutex类的对象相对应,使用互斥对象时,必须创建一个CSingleLock或CMultiLock对象,用于实际的访问控制,因为这里的例子只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock()函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:
#include "afxmt.h"
int array[10],destarray[10];
CMutex Section;
/////////////////////////////////////////////////////////////
UINT WriteThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();
for(int x=0;x<10;x++)
array[x]=x;
singlelock.Unlock();
}
UINT ReadThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();
For(int x=0;x<10;x++)
Destarray[x]=array[x];
singlelock.Unlock();
}
互斥的另一个应用,保证只能创建一个进程。
xxx::OnInitDialog() //xxx::InitInstance()
{
HANDLE dlgHandle = ::CreateMutex( NULL, FALSE, _T("ProcessTest.exe"));
if( GetLastError() == ERROR_ALREADY_EXISTS )
{
AfxMessageBox("该应用程序已经存在!");
this->EndDialog( 1 ); // return FALSE;
}
CloseHandle( dlgHandle );
}
Mutex跟criticalSection的区别?
1锁mutex的时间是锁criticalSection时间的100倍。
2 mutex可以跨进程。
3等mutex可以指定“结束等待”的时间长度。
3.信号量
以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
信号量的用法和互斥的用法很相似,也可以跨进程,不同的是它可以同一时刻允许多个线程访问同一个资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦创建了一个信号量对象,就可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对象,然后用该对象的Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得以显示。
CSemaphore (LONG lInitialCount=1, LONG lMaxCount=1,
LPCTSTR pstrName=NULL, LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
· lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值;
· lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目,设个很大值就可以。
· 后两个参数在同一进程中使用一般为NULL,不作过多讨论;
/////////////////////////////////////////////////////////////////
Csemaphore *semaphore;
Semaphore=new Csemaphore(2,2);
(用后得删除delete ptrSemaphore;)
HWND hWnd=GetSafeHwnd();
AfxBeginThread(threadProc1,hWnd);
AfxBeginThread(threadProc2,hWnd);
AfxBeginThread(threadProc3,hWnd);
//////////////////////////////////////////////////////////////////////
UINT ThreadProc1(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);
return 0;
}
UINT ThreadProc2(LPVOID param)
{CSingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);
return 0;
}
UINT ThreadProc3(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);
return 0;
}
4.事件对象(Event)
事件对象与前面所奖的几个同步对象有很大区别,当在线程访问某一资源之前,还需要等待某一时间的发生,就需要用到事件对象,例如:拷贝数据到数据文档时,线程应被通知何时数据是可用的。支持多进程命名Event,不支持未命名Event。
在mfc中,event对象是有cevent类来创建。一个event对象可以有两种状态:有信号状态(signaled)和无信号状态(nosignaled)。线程通过检验event对象的状态进行相应的处理。
(1)创建一个event对象和生命一个全局变量一样简单,如:
cevent startthread;
(2)在创建完之后,event对象就自动处于无信号状态,要给event对象发送信号,需要调用event对象的setevent成员函数,如:
startthread.setevent();
(3)执行该函数后,event对象就会处于有信号状态,这时,线程应该对该信号进行检测,以便知道event对象处于有信号状态了,线程通过调用API函数waitforsingleobject来检测事件的信号。如:
::waitforsingleobject(startthread.m_hObject,INFINITE);
此函数的两个参数分别是要检验的event对象的句柄(在该event对象的m_hObject成员变量中)和等待该信号准备花的时间,INFINITE表示无时间限制即无期限的等待,如果把上面的代码放到线程处理函数的开始部分,则系统会挂起该线程,知道event对象处于有信号状态。
当event对象信号打开后,所有等待这个事件的线程都将接收到这个信号,并开始执行;当信号关闭后,所有等待这个事件的线程都将被挂起,直到这个事件的信号被改变为止。