在程序中使用多线程时,一般很少有多个线程能在其生命周期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应该在其处理任务完成后进行。
用于同步的对象主要是:信号量、互拆锁、临界区、事件。
1、信号量
为了限制使用共享资源的线程数目,我们应该使用信号量。信号量是一个内核对象。它存储了一个计数器变量来跟踪使用共享资源的线程数目。例如下面的代码使用了CSemaphore类创建了一个信号量对象,它确保在指定的时间间隔内(由构造函数第一个参数指定)最多只有5个线程能使用共享资源。还假定初始时没有线程获得资源
CSemaphore g_Sem(5,5)
一旦线程访问共享资源,信号量的计数器就减去1。若减到0,则接下来对资源的访问就会被拒绝,直到有一个持有资源的线程离开(也就是释放信号量)。
//尝试使用共享资源
::WaitForSingleObject(g_Sem,INFINITE);
//信号量减去1
.........................................................
//使用共享资源
..........................................................
//任务完成后,让位其他线程
::ReleaseSemaPhore(g_Sem,1,NULL);
//信号量加1
2、互拆锁
互拆锁设计为对同步访问共享资源进行保护。互拆锁在内核中实现,因此需要进入内核模式操纵它们。互拆锁不仅能在不同线程之间,也可以在不同进程之间应用。要跨进程使用,则互拆锁应该是有名字的。这样就可以在不同进程之间用名字调用。MFC中使用CMutex类来操作互拆锁。方法如下:
CSingleLock singleLock(&m_Mutex);
singleLock.Lock() //保护共享资源
if(singlelock.Locked())
{
//使用共享资源
//使用完成后,让给其他线程
singleLock.Unlock();
}
3、临界区
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源内进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将会被挂起,并一直持续到进入临界区的线程离开。临界区再被释放后,其他的线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。MFC为临界区提供了一个CCriticalSection类,使用该类进行线程同步时非常简单,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护的代码片段即可。
//MFC临界区类对象
CCriticalSection g_clsCriticalSection;
//共享资源
Char g_cArray[10];
UINT ThreadPro(LPVOID pParam)
{
//进入临界区
g_clsCriticalSection.Lock();
//对共享资源进行写入操作
for(int i=0;i<10;i++)
{
g_cArray[i]='a';
Sleep(1);
}
//离开临界区
g_clsCriticalSection.UnLock();
return 0;
}
4、事件
一般来说,事件是用于这样的情形下:当指定的动作发生后,一个线程(或者多个线程)才开始执行其任务。例如:一个线程可能等待必需的数据收集完成后才开始将其保存到硬盘上。
有两种事件:手动重置型和自动重置型。通过使用事件,我们可以轻松的通知另一个线程特定的动作已经发生了。对于手动重置型事件线程使用它通知多个线程特定动作已经发生;而对自动重置型事件,线程使用它只可以通知一个线程。
MFC中,CEvent类封装了事件对象(若在Win32中,它是用一个HANDLE来表示)。CEvent构造函数运行我们选择创建手动重置型和自动重置型事件。默认的创建类型是自动重置型事件。为了通知正在等待的线程,我们可以调用CEvent::SetEvent方法,这个方法将会让事件进入已通知状态。
若事件是手动重置型,则事件会保持已通知状态,直到对应的CEvent::ResetEvent被调用,这个方法将使事件进入未通知状态。这个特性使得一个线程可以通过一个SetEvent调用取通知多个线程。
若事件是自动重置型的,则所有正在等待的线程中只有一个线程会接到通知。当那个线程接收到通知后,事件会自动进入为通知状态。
CEvent g_eventStart;
UINT ThreadProc1(LPVOID pPrarm)
{
::WaitForSingleObject(g_eventStart,INFINITE);
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart,INFINITE);
return 0;
}
在这个例子中一个全局的CEvent对象被创建,它是自动重置型的。除此之外,有两个工作线程在等待这个事件对象以便开始其工作。只要第三个线程调用那个事件对象的SetEvent方法,则两个线程之一(当然没人知道会是哪个)会接收到通知,然后事件会进入未通知状态,这就防止了第二个线程也得到事件通知。
CEvent g_eventStart(FALSE,TRUE);
UINT ThreadProc1(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart,INFINITE);
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart,INFINITE);
return 0;
}
这段代码和上面的稍有不同,CEvent对象构造函数的参数不一样,这是一个手动重置型事件对象。若第三个线程调用事件对象的SetEvent方法,则可以确保两个工作线程都会同时(几乎是同时)开始工作。这是因为手动重置型事件在进入已通知状态后,会保持此状态直到对应的ResetEvent被调用。
除此之外事件对象还有一个方法:CEvent::PlusEvent。这个方法首先使得事件对象进入已通知状态,然后使其退回到未通知状态。若事件是手动重置型,事件进入已通知状态让所有正在等待的线程得到通知,然后事件进入未通知状态。若事件是自动重置型的,事件进入已通知状态时,只会让所有等待的线程之一得到通知。若没有线程等待,则调用ResetEvent方法什么也不做。