Windows上多线程同步相关的MFC类
相关类
MFC中提供的用于多线程程序中进行线程同步的类可以分为两类:同步类和同步访问类。
同步类 synchronization class | 用于确保对资源完整性访问进行控制的类 | |
同步访问类 Synchronization access class | 用于获取受控资源访问权限的类 |
上面表格中的CSyncObject类为纯虚类,在实际中几乎不会直接使用它。它也是其它同步类的直接父类。
那么,在实际中应该如何判断到底需要使用上面的那个同步类了?一般遵循一下的原则:
1. 如果线程对资源的访问必须是在某个事件发生之后,那么可以使用CEvent。
2. 如果同一个程序中的多个线程可以在同一时刻访问资源,那么可以使用CSemaphore。
3. 如果多个程序可以访问资源,则可以使用CMutex;否则,使用CCriticalSection。
程序示例
CEvent
CEvent在线程必须等待某个事件发生后才能继续执行的情况下非常有用。它有两种工作模式:手动和自动。对于手动模式来说,事件的状态会一直保持到下一次调用SetEvent 或者 ResetEvent ;对于自动模式来说,事件的状态会在唤醒至少一个线程之后重新恢复到不可用的状态。
下面的代码演示了CEvent的典型用法。程序中通过两个线程来分别输出20个a和b字符,但是我们期待这两个字符的输出是相间的,也就是不要出现连续的a或者连续的b。
Visual Studio创建的控制台程序,支持MFC:
// CEventDemo.cpp :定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "CEventDemo.h"
#include "afxmt.h"
#include <conio.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 唯一的应用程序对象
CWinApp theApp;
using namespacestd;
CEvent TrigerB(false);//用来唤醒线程B,来输出字符b
CEvent TrigerA(true);//用来唤醒线程A,来输出字符a
//线程A:用来输出字符a
UINT __cdecl ThreadA( LPVOIDpParam )
{
for( int count = 0; count < 20; count++)
{
WaitForSingleObject(TrigerA.m_hObject,INFINITE);//等待被唤醒
cout<< " a " ;
TrigerA.ResetEvent();//重置,防止该线程输出连续的a
//TrigerA.PulseEvent();
TrigerB.SetEvent();//唤醒线程B
}
return 0;
}
//线程B:用来输出字符b
UINT __cdecl ThreadB( LPVOIDpParam )
{
for( int count = 0; count < 20; count++)
{
WaitForSingleObject(TrigerB.m_hObject,INFINITE);//等待被唤醒
cout<< " b " ;
TrigerB.ResetEvent();//重置,防止该线程输出连续的b
//TrigerB.PulseEvent();
TrigerA.SetEvent();//唤醒线程A
}
return 0;
}
int _tmain(intargc,TCHAR*argv[],TCHAR*envp[])
{
int nRetCode = 0;
// 初始化MFC并在失败时显示错误
if (!AfxWinInit(::GetModuleHandle(NULL),NULL, ::GetCommandLine(), 0))
{
// TODO: 更改错误代码以符合您的需要
_tprintf(_T("错误: MFC初始化失败\n"));
nRetCode= 1;
}
else
{
// TODO: 在此处为应用程序的行为编写代码。
CWinThread* threadA = AfxBeginThread(ThreadB,NULL);
CWinThread* threadB = AfxBeginThread(ThreadA,NULL);
WaitForSingleObject(threadB->m_hThread,INFINITE);
WaitForSingleObject(threadA->m_hThread,INFINITE);
char c = getch();
}
return nRetCode;
}
CCriticalSection
CCriticalSection主要用于资源在同一个时刻只允许一个线程访问的情况;也就是主要用于临界区对临界资源的访问。
示例程序如下,Visual Studio创建的控制台程序,支持MFC:
//CCriticalSectionDemo.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "CCriticalSectionDemo.h"
#include "afxmt.h"
#include <conio.h>
#include <windows.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 唯一的应用程序对象
CWinApp theApp;
using namespacestd;
const intsize= 10;
const inttotalCount= 10;
int data[size] = { 0 };
CCriticalSection cs;
//线程A:用来输出数组中的全部数值;输出次
UINT __cdecl PrintThread( LPVOIDpParam )
{
for ( int count = 0; count < totalCount;count++)
{
cs.Lock();//每次输出之前,先申请访问临界区
for( int index = 0; index < size;index++)
{
cout<< data[index]<< " ";
Sleep(200);
}
cout<< endl;
cs.Unlock();//资源访问完毕后,释放之
}
return 0;
}
//线程B:用来修改数组中的全部数值
UINT __cdecl ModifyThread( LPVOIDpParam )
{
for ( int count = 0; count < totalCount;count++)
{
cs.Lock();//每次修改资源之前,申请先
for( int index = 0; index < size;index++)
{
data[index] =count;
Sleep(200);
}
cs.Unlock();//资源使用完毕,释放之
}
return 0;
}
int _tmain(intargc,TCHAR*argv[],TCHAR*envp[])
{
int nRetCode = 0;
// 初始化MFC并在失败时显示错误
if (!AfxWinInit(::GetModuleHandle(NULL),NULL, ::GetCommandLine(), 0))
{
// TODO: 更改错误代码以符合您的需要
_tprintf(_T("错误: MFC初始化失败\n"));
nRetCode= 1;
}
else
{
// TODO: 在此处为应用程序的行为编写代码。
CWinThread* pPrintThread = AfxBeginThread(PrintThread,NULL);
CWinThread* pModifyThread = AfxBeginThread(ModifyThread,NULL);
WaitForSingleObject(pPrintThread->m_hThread,INFINITE);
WaitForSingleObject(pModifyThread->m_hThread,INFINITE);
char c = getch();
}
return nRetCode;
}
上面程序的可能输出如下:
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 2 2
3 3 3 3 3 3 3 3 3 3
4 4 4 4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5 5
6 6 6 6 6 6 6 6 6 6
7 7 7 7 7 7 7 7 7 7
8 8 8 8 8 8 8 8 8 8
如果去掉程序中所有的Lock和Unlock的调用,可能的输出则如下:
0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 0 1 1 1
2 2 2 2 2 2 2 2 2 2
3 3 3 3 3 3 3 3 3 3
4 4 4 4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5 5
6 6 6 6 6 6 6 6 6 6
6 6 7 7 7 7 7 7 7 7
8 8 8 8 8 8 8 8 7 7
9 9 9 9 9 9 9 9 9 9
注意其中的第二行,第八行和第九行的输出,这样的输出没有保证打印线程和修改线程对资源访问的完整性。也就是说出现了,修改线程和打印线程对资源访问的交叉。
增加了cs的Lock和Unlock操作之后,就避免了这种情况的发生。
注意:增加了cs的操作并不能保证打印操作总是在每次修改之后就进行的,想要输出:
0 0 0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 2 2
3 3 3 3 3 3 3 3 3 3
4 4 4 4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5 5
6 6 6 6 6 6 6 6 6 6
7 7 7 7 7 7 7 7 7 7
8 8 8 8 8 8 8 8 8 8
9 9 9 9 9 9 9 9 9 9
则可以使用前面说到的CEvent,因为打印线程总是要在每次修改操作完成之后进行的。