多线程同步方式一:临界段
1.常见问题
在一个进程中,多个线程是共享进程资源的,而由于线程的并发操作,如果不加控制,就有可能出现多个线程同时操作共享资源的情况,而造成操作混乱或出差等。有可能你不太相信,请看下面的一个实例,你可能会真实感受到问题的所在。
实例1说明:一个线程一次产生5个相同的随机数,另一个线程将这5个数输出。
//.h文件
//声明线程处理函数
unsigned __stdcall WriteThread( void*pArguments );//产生数据的线程函数
unsigned __stdcallReadThread(void*pArguments );//读取数据的线程函数
//.cpp文件
//开始/停止
void CProbem1Dlg::OnBnClickedButtonDo()
{
// TODO: 在此添加控件通知处理程序代码
CString szText;
GetDlgItemText(IDC_BUTTON_DO,szText);
if(szText =="开始")
{
g_bRun = true;
SetDlgItemText(IDC_BUTTON_DO,"停止");
//创建产生随机数的线程
_beginthreadex(NULL,0,WriteThread,NULL,0,NULL);
//创建读取数据数的线程
_beginthreadex(NULL,0,ReadThread,&m_ctlList,0,NULL);
}
else
{
g_bRun = false;
SetDlgItemText(IDC_BUTTON_DO,"开始");
}
}
//产生随机数的线程函数
unsigned __stdcall WriteThread( void*pArguments )
{
int num = 1;
while(g_bRun)
{
num = 100+rand()%900;//产生位数的随机数
for(inti = 0;i < 5;i++)
{
g_nBuff[i] =num;
}
}
return 0;
}
//读取随机数的线程函数
unsigned __stdcall ReadThread( void* pArguments )
{
CListBox *pList= (CListBox*)pArguments;
CString szTemp;
while(g_bRun)
{
szTemp.Format("%d %d %d %d %d",g_nBuff[0],g_nBuff[1],g_nBuff[2],g_nBuff[3],g_nBuff[4]);
pList->AddString(szTemp);
}
return 0;
}
运行结果:(错误用红色标出)
工程源码下载:http://download.csdn.net/detail/cbnotes/5045867
===========================================================================
实例2说明:线程报数问题,前面已经介绍过,再次演示下。
该实例的详细说明和演示请看上一章节《线程间的同步概述》的前言部分,不再累述。
工程源码下载:http://download.csdn.net/detail/cbnotes/5015914
你已经看到了,多线程编程如果不控制好同步问题,将给你的程序带来各种意想不到的问题和错误,而有些错误还是不容易发觉的,可能要通过成千上万次的运行才会显现出来。所以说线程同步是多线程编程的核心和难点,那怎么才能控制好同步呢?方法不是唯一的,按你的喜好选择。今天先给大家介绍我认为是最简单实用的同步方式:临界段。
2.临界段
临界段(CriticalSection),也有人称临界区,是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
通俗点讲,临界段就像一间门很窄的房间,房间里面放了一些大家都喜欢的东西,但由于门很窄,每次只能一个人进去,并把门关闭,其它的人只能在门外排队,只有当进去的人出来了,下一人才能进去。也可以比喻成一把的钥匙,只有拥有了这把钥匙,才拥有了对共享资源的控制权,如果其它线程要访问上锁的共享资源,就必须首先获取这把钥匙,如果这把钥匙在其它线程手里,它就必须等待,直到另外一个线程交出钥匙为止。
临界段的使用方式有Win32 API方式和MFC类方式两种,下面分别介绍。
3.用Win32 API方式使用临界段
Win32API中与临界段有关的函数如下表所示:
序号 | 名 称 | 作 用 |
1 | InitializeCriticalSection() | 创建并初始化一个临界段 |
2 | DeleteCriticalSection() | 释放一个临界段 |
3 | EnterCriticalSection() | 获取对临界段的所有权,独占共享资源 |
4 | TryEnterCriticalection() | 试图获得对临界段的所有权,但不阻塞 |
5 | LeaveCriticalSection() | 释放对共享资源的所有权 |
别看只有5个函数,但个个强悍。关于这5个函数的使用和详细介绍请参考MSDN,我就不在此献丑了。
注意1:临界段在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去获取和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
注意2:在使用临界段时,一般不允许其运行时间过长,只要进入临界段的线程还没有离开,其他所有试图进入此临界段的线程都会被挂起而进入到等待状态,并会在一定程度上影响程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界段。如果进入了临界段却一直没有释放,同样也会引起其他线程的长时间等待。换句话说,在执行了EnterCriticalSection()语句进入临界段后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句的执行。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
下面就以实例来介绍这些函数的使用。我就改写上面第一个例子,加入临界段同步控制,你可以和没有加入同步控制的比较下。
主要程序代码:
1. 在.CPP文件头加入两个全局变量:
bool g_bSyn = false; //同步控制
CRITICAL_SECTION g_cs;// 临界区结构对象
2. 在OnInitDialog()函数中加入下面两条语句:
((CButton*)GetDlgItem(IDC_CHECK1))->SetCheck(BST_CHECKED);//同步状态
InitializeCriticalSection(&g_cs);//初始化临界段对象
3. 修改线程函数:
//开始/停止
void CProbem1Dlg::OnBnClickedButtonDo()
{
// TODO: 在此添加控件通知处理程序代码
CString szText;
GetDlgItemText(IDC_BUTTON_DO,szText);
if(szText =="开始")
{
g_bSyn = ((CButton*)GetDlgItem(IDC_CHECK1))->GetCheck();//得到同步状态
GetDlgItem(IDC_CHECK1)->EnableWindow(FALSE);//运行中禁止控制同步状态
m_ctlList.ResetContent();//清空列表数据
g_bRun = true;
SetDlgItemText(IDC_BUTTON_DO,"停止");
//创建产生随机数的线程
_beginthreadex(NULL,0,WriteThread,NULL,0,NULL);
//创建读取数据数的线程
_beginthreadex(NULL,0,ReadThread,&m_ctlList,0,NULL);
}
else
{
g_bRun = false;
SetDlgItemText(IDC_BUTTON_DO,"开始");
GetDlgItem(IDC_CHECK1)->EnableWindow(TRUE);
}
}
//产生随机数的线程函数
unsigned __stdcall WriteThread( void*pArguments )
{
int num = 1;
while(g_bRun)
{
num = 100+rand()%900;//产生位数的随机数
if(g_bSyn)
{//同步控制
EnterCriticalSection(&g_cs);//进入临界段
for(inti = 0;i < 5;i++)
{
g_nBuff[i] =num;
}
LeaveCriticalSection(&g_cs);//离开临界段
}
else
{//没有同步控制
for(inti = 0;i < 5;i++)
{
g_nBuff[i] =num;
}
}
}
return 0;
}
//读取随机数的线程函数
unsigned __stdcall ReadThread( void* pArguments )
{
CListBox *pList= (CListBox*)pArguments;
CString szTemp;
while(g_bRun)
{
if(g_bSyn)
{//同步控制
EnterCriticalSection(&g_cs);//进入临界段
szTemp.Format("%d %d %d %d %d",g_nBuff[0],g_nBuff[1],g_nBuff[2],g_nBuff[3],g_nBuff[4]);
LeaveCriticalSection(&g_cs);//离开临界段
pList->AddString(szTemp);
}
else
{//没有同步控制
szTemp.Format("%d %d %d %d %d",g_nBuff[0],g_nBuff[1],g_nBuff[2],g_nBuff[3],g_nBuff[4]);
pList->AddString(szTemp);
}
}
return 0;
}
程序运行结果如下所示:
工程源码打包下载地址:http://download.csdn.net/detail/cbnotes/5069267
可以看出采用临界段API控制多线程同步还是比较简单有效的。
4.用MFC类方式使用临界段
在MFC中,CCriticalSection类对临界段进行了封装。它从CSyncObject类继承而来,其在MFC结构中的位置如下图所示。
CCriticalSection类的定义和实现在此就不再累述,有需要的请查看MSDN。
简单点说,CCriticalSection就是对上面的5个API的简单封装,并换了一个名字。CCriticalSection类主要有两个成员函数Lock()和Unlock()。Lock()函数用于获得对临界段对象的所有权,Unlock()函数用于释放对临界段对象的所有权。
该类的实现在afxmt.inl文件中,其实现为内联函数,因此其效率是比较高的。
CCriticalSection类的用法分为2种情况,一种是单独使用,另一种是和CSingleLock类配合使用。下面将分别进行介绍。
先来看CCriticalSection类的单独使用,首先要创建一个临界段对象,然后在需要访问临界段时调用该对象的Lock()函数进行锁定操作,在使用完后再调用该对象的Unlock()函数进行解锁操作。
还是用实例来解说比较有说服力,还是以上面的例子来修改。采用CCriticalSection类来同步。和前面的采用Win32 API很相像。
主要程序代码:
1.在.CPP文件头加入一个全局变量:
CCriticalSection g_cs;// 临界段对象
2.改写线程函数
//产生随机数的线程函数
unsigned __stdcall WriteThread( void*pArguments )
{
int num = 1;
while(g_bRun)
{
num = 100+rand()%900;//产生位数的随机数
if(g_bSyn)
{//同步控制
g_cs.Lock();//进入临界段
for(inti = 0;i < 5;i++)
{
g_nBuff[i] =num;
}
g_cs.Unlock();//离开临界段
}
else
{//没有同步控制
for(inti = 0;i < 5;i++)
{
g_nBuff[i] =num;
}
}
}
return 0;
}
//读取随机数的线程函数
unsigned __stdcall ReadThread( void* pArguments )
{
CListBox *pList= (CListBox*)pArguments;
CString szTemp;
while(g_bRun)
{
if(g_bSyn)
{//同步控制
g_cs.Lock();//进入临界段
szTemp.Format("%d %d %d %d %d",g_nBuff[0],g_nBuff[1],g_nBuff[2],g_nBuff[3],g_nBuff[4]);
g_cs.Unlock();//离开临界段
pList->AddString(szTemp);
}
else
{//没有同步控制
szTemp.Format("%d %d %d %d %d",g_nBuff[0],g_nBuff[1],g_nBuff[2],g_nBuff[3],g_nBuff[4]);
pList->AddString(szTemp);
}
}
return 0;
}
程序运行结果如下所示:
工程源码打包下载地址:http://download.csdn.net/detail/cbnotes/5069514
关于CCriticalSection类的第2种用法,和CSingleLock类配合使用,将在以后的章节中专门详细介绍,在此就不重复了,你可以不必太关心这种用法的。
===========================================
转载请标明出处,谢谢。http://blog.csdn.net/cbNotes
===========================================