VC 线程同步

来源:百度文库 编辑:神马文学网 时间:2024/05/15 17:31:08
临界区
管理事件内核对象
信号量内核对象
互斥内核对象
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程同步是一个非常大的话题,包括方方面面的内容。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
MFC为事件相关处理也提供了一个CEvent类,共包含有除构造函数外的4个成员函数PulseEvent()、ResetEvent()、SetEvent()和UnLock()。在功能上分别相当与Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函数。而构造函数则履行了原CreateEvent()函数创建事件对象的职责,其函数原型为:
CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
按照此缺省设置将创建一个自动复位、初始状态为复位状态的没有名字的事件对象。封装后的CEvent类使用起来更加方便,图2即展示了CEvent类对A、B两线程的同步过程:
CEvent cEvent;
static UINT thread3(LPVOID pParam)   // thread3优先于thread4,即thread3执行完后在执行thread4
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"A");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
cEvent.SetEvent();
return 0;
}
static UINT thread4(LPVOID pParam)
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
cEvent.Lock();
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"B");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
cEvent.SetEvent();
return 0;
}
互斥对象在MFC中通过CMutex类进行表述。使用CMutex类的方法非常简单,在构造CMutex类对象的同时可以指明待查询的互斥对象的名字,在构造函数返回后即可访问此互斥变量。CMutex类也是只含有构造函数这唯一的成员函数,当完成对互斥对象保护资源的访问后,可通过调用从父类CSyncObject继承的UnLock()函数完成对互斥对象的释放。CMutex类构造函数原型为:
CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
该类的适用范围和实现原理与API方式创建的互斥内核对象是完全类似的,但要简洁的多,下面给出就是对前面的示例代码经CMutex类改写后的程序实现清单:
CMutex cMutex(FALSE,NULL);
static UINT thread3(LPVOID pParam)  // thread3优先于thread4,即thread3执行完后在执行thread4或是thread4优先于thread3,即thread4执行完后在执行thread4
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
cMutex.Lock();
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"A");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
cMutex.UnLock();
return 0;
}
static UINT thread4(LPVOID pParam)
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
cEvent.Lock();
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"B");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
cMutex.UnLock();
return 0;
}
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:
CRITICAL_SECTION  hCritial;
static UINT thread3(LPVOID pParam)  // thread3优先于thread4,即thread3执行完后在执行thread4或是thread4优先于thread3,即thread4执行完后在执行thread4
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
EnterCritialSection(&hCritial);
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"A");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
LeaveCritialSection(&hCritial);
return 0;
}
static UINT thread4(LPVOID pParam)
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
EnterCritialSection(&hCritial);
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"B");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
LeaveCritialSection(&hCritial);
return 0;
}
一个CSemaphore类对象代表一个“信号”——一个同步对象,它允许有限数目的线程在一个或多个进程中访问同一个资源。一个CSemaphore对象保持了对当前访问某一指定资源的线程的计数。对于一个只能支持有限数目用户的共享资源来说,CSemaphore是很有用的。
HANDLE hSema;
hSema = CresteSemaphore(NULL,1,1,NULL); //若CresteSemaphore(NULL,2,2,NULL),则thread3和thread4都可以执行,但是如果在再有thread5的话,thread5必须在thread3或thread4才会执行
static UINT thread3(LPVOID pParam)  // thread3优先于thread4,即thread3执行完后在执行thread4或是thread4优先于thread3,即thread4执行完后在执行thread4
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
WaitForSingleObject(hSema,INFINITE);
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"A");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
ReleaseSemaphore(hSema,1,NULL);
return 0;
}
static UINT thread4(LPVOID pParam)
{
CEdit *p=(CEdit*)pParam;
char buf[MAX_PATH];
WaitForSingleObject(hSema,INFINITE);
for(int i=0;i<20;i++)
{
::SendMessage(p->GetSafeHwnd(),WM_GETTEXT,MAX_PATH,(LPARAM)buf);
strcat(buf,"B");
::SendMessage(p->GetSafeHwnd(),WM_SETTEXT,0,(LPARAM)buf);
Sleep(200);
}
ReleaseSemaphore(hSema,1,NULL);
return 0;
}
void CEventSynchDlg::OnStartThread3()
{
AfxBeginThread(thread3,&m_result);
}
void CEventSynchDlg::OnStartThread4()
{
AfxBeginThread(thread4,&m_result);
}
HANDLE hMutex=CreateMutex(NULL,TRUE,LPCTSTR("tickets"));
DWORD CCreateMultiThreadDlg::ThreadFunOne(LPVOID lpParam)
{
WaitForSingleObject(hMutex,INFINITE);
CCreateMultiThreadDlg* pDlg = (CCreateMultiThreadDlg*)lpParam;
int low,high,pos;
pos = pDlg->m_ThreadOne.GetPos();
pDlg->m_ThreadOne.GetRange(low,high);
while (pos{
pos = pDlg->m_ThreadOne.GetPos();
pDlg->m_ThreadOne.SetPos(pos+1);
}
pDlg->m_ThreadOne.SetPos(0);
return 0;
ReleaseMutex(hMutex);
}
HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,"event")
DWORD CCreateMultiThreadDlg::ThreadFunTWO(LPVOID lpParam)
{
WaitForSingleObject(hEvent,INFINITE);
CCreateMultiThreadDlg* pDlg = (CCreateMultiThreadDlg*)lpParam;
int low,high,pos;
pos = pDlg->m_ThreadOne.GetPos();
pDlg->m_ThreadOne.GetRange(low,high);
while (pos{
pos = pDlg->m_ThreadOne.GetPos();
pDlg->m_ThreadOne.SetPos(pos+1);
}
pDlg->m_ThreadOne.SetPos(0);
return 0;
SetEvent(hEvent );
}
// CreateMutex(LPSECURITY lp. BOOL bInitialOwner,LPCTSTR lpName); bInitialOwner为FALSE ,则WaitForSingleObject(hMutex,INFINITE);可以通过。
否则必须调ReleaseMutex(hMutex);后WaitForSingleObject(hMutex,INFINITE);才可以通过
void CCreateMultiThreadDlg::OnCreatethread()
{
hMutex = CreateMutex(NULL,true,"mutex");  //或者直接hMutex = CreateMutex(NULL,false,"mutex");  不需要ReleaseMutex(hMutex);
m_hThreadOne = CreateThread(NULL,100,ThreadFunOne,(void*)this,CREATE_SUSPENDED,NULL);
SetThreadPriority(m_hThreadOne,THREAD_PRIORITY_ABOVE_NORMAL);
ReleaseMutex(hMutex);
}
// CreateEvent(LPSECURITY lp. BOOL bManuaReset,BOOL bInitialState,LPCTSTR lpName);
如果bManuaReset为FALSE,bInitialState为FALSE ,则WaitForSingleObject(hMutex,INFINITE);不可以通过,必须调SetEvent才可以通过
如果bManuaReset为FALSE,bInitialState为TRUE ,则WaitForSingleObject(hMutex,INFINITE);可以通过。
如果bManuaReset为TRUE,bInitialState为TRUE ,则WaitForSingleObject(hMutex,INFINITE);可以通过,但必须要调ResetEvent()才会有互斥效果
bManuaReset为TRUE,bInitialState为FALSE的情况一般不出现。
因为bManualReset   如果true,人工复位,   一旦该Event被设置为有信号,则它一直会等到ResetEvent()API被调用时才会恢复为无信号.
如果为false,Event被设置为有信号,则当执行完WaitForSingleObject()后,   该Event就会自动复位,变成无信号.
void CCreateMultiThreadDlg::OnCreatethread()
{
hEvent = CreateEvent(NULL,TRUE,TRUE,"event");  //或者直接hEvent = CreateEvent(NULL,FALSE,TRUE,"event");   不需要SetMutex(hMutex);
//如果hEvent = CreateEvent(NULL,FALSE,FALSE,"event");   需要SetMutex(hMutex);
m_hThreadOne = CreateThread(NULL,100,ThreadFunOne,(void*)this,CREATE_SUSPENDED,NULL);
SetThreadPriority(m_hThreadOne,THREAD_PRIORITY_ABOVE_NORMAL);
ResetEvent(hEvent);
}
void CCreateMultiThreadDlg::OnRunthread()
{
ResumeThread(m_hThreadOne);
}
void CCreateMultiThreadDlg::OnSuspendthread()
{
SuspendThread(m_hThreadOne);
}
总结:
1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。
4.    信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。