MFC 提供的多线程类分为两类:同步对象(CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步访问对象(CMultiLock 和 CSingleLock)。
必须控制对资源的访问权限以确保资源的完整性时,使用同步类。 同步访问类用于获取对这些受控资源的访问权限。
要使类完全线程安全,请先将适当的同步类作为数据成员添加到共享类。 在先前的帐户管理示例中,CSemaphore 数据成员添加到视图类中,CCriticalSection 数据成员添加到链接列表类中,CEvent 数据成员添加到数据存储类中。
接下来,向修改类中的数据或访问受控资源的所有成员函数添加同步调用。在每个函数中,应创建 CSingleLock 或 CMultiLock 对象,并调用该对象的 Lock 函数。 当锁对象超出范围并被销毁时,对象的析构函数会调用 Unlock,从而释放资源。 当然,如果需要,你可直接调用 Unlock。
临界区
CCriticalSection类
表示一个"临界区", 一次支持一个线程访问资源或代码段的同步对象。
| 名称 | 描述 |
|---|---|
| CCriticalSection::Lock | 用于获取对 CCriticalSection 对象的访问权限。 |
| CCriticalSection::Unlock | 释放 CCriticalSection 对象。 |
访问前用Lock()锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock().
可通过两种方法使用 CCriticalSection 对象:独立和嵌入类中。
独立方法 若要使用独立 CCriticalSection 对象,请根据需要构造 CCriticalSection 对象。 从构造函数成功返回后,通过调用 Lock 显式锁定对象。 访问关键部分后,调用 Unlock。
嵌入方法 还可以通过将 CCriticalSection 类型的数据成员添加到类并在需要时锁定数据成员来与多个线程共享该类。
步骤
1)定义全局变量临界区。CCriticalSection criticalSection;//定义全局变量临界区
2)操作共享资源前,用criticalSection.Lock()锁定,防止其他线程访问。
3)操作共享资源后,用criticalSection.Unlock()解锁,让其他线程可以访问
范例
新建一个MFC对话框项目,添加按钮和编辑框















cpp
CCriticalSection criticalSection;//定义全局变量临界区
char16_t g_Array[10];//共享字符串组
....
UINT WriteA(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'A';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
criticalSection.Unlock();
return 0;
}
UINT WriteB(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'B';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
criticalSection.Unlock();
return 0;
}
....
void CMFCAPPDlg::OnClickedButton1()
{
AfxBeginThread(WriteA, &m_CtrlA);
}
void CMFCAPPDlg::OnClickedButton2()
{
AfxBeginThread(WriteB, &m_CtrlB);
}


互斥量
CMutex类
一个允许一个线程以互相排斥的方式访问一个资源的同步对象。
当一次只允许一个线程修改数据或其他一些受控资源时,Mutex 非常有用。
若要使用 CMutex 对象,请根据需要构造 CMutex 对象。 指定要等待的 mutex 的名称,并指定应用程序最初应拥有它。 然后,即可在构造函数返回时访问该 mutex。 完成访问受控资源后,调用 CSyncObject::Unlock。
使用 CMutex 对象的另一种方法是将 CMutex 类型的变量作为数据成员添加到要控制的类。 在构造受控对象期间,调用 CMutex 数据成员的构造函数,指定 mutex 是否最初拥有、mutex 的名称(如果将跨进程边界使用)和所需的安全属性。
若要以这种方式访问 CMutex 对象控制的资源,请先在资源的访问成员函数中创建 CSingleLock 类型或 CMultiLock 类型的变量。 然后调用锁对象的 Lock 成员函数(例如 CSingleLock::Lock)。 此时,线程将获取对资源的访问权限,等待资源释放并获取访问权限,或等待资源释放并超时而无法访问资源。 在任何情况下,都是以线程安全的方式访问资源。 若要释放资源,请使用锁对象的 Unlock 成员函数(例如,CSingleLock::Unlock),或允许锁对象脱离范围。
CSingleLock 类
表示多线程程序中用于控制对一个资源的访问的访问控制机制。
| 名称 | 描述 |
|---|---|
| CSingleLock::IsLocked | 确定对象是否已锁定。 |
| CSingleLock::Lock | 等待同步对象。 |
| CSingleLock::Unlock | 释放同步对象。 |
若要使用同步类 CSemaphore、CMutex、CCriticalSection 和 CEvent,必须创建 CSingleLock 或 CMultiLock 对象来等待并释放同步对象。 如果一次只需等待一个对象,则使用 CSingleLock。 如果有多个对象可在特定时间使用,则使用 CMultiLock。
若要使用 CSingleLock 对象,请在受控资源的类中的构造函数内调用其成员函数。 然后调用 IsLocked 成员函数以确定资源是否可用。 如果可用,请继续执行成员函数的其余部分。 如果资源不可用,请等待指定的时间以便系统释放资源,或返回失败。 不再需要使用资源后,如果要再次使用 CSingleLock 对象,则调用 Unlock 函数,否则允许销毁 CSingleLock 对象。
CMultiLock 类
表示多线程程序中用于控制对多个资源的访问的访问控制机制。
| 名称 | 描述 |
|---|---|
| CMultiLock::IsLocked | 确定数组中的特定同步对象是否锁定。 |
| CMultiLock::Lock | 等待同步对象的数组。 |
| CMultiLock::Unlock | 释放任何拥有的同步对象。 |
若要使用同步类 CSemaphore、CMutex 和 CEvent,可以创建 CMultiLock 或 CSingleLock 对象来等待并释放同步对象。 如果有多个对象可在特定时间使用,则使用 CMultiLock。 如果一次只需等待一个对象,则使用 CSingleLock。
若要使用 CMultiLock 对象,请先创建要等待的同步对象的数组。 接下来在受控资源的类中成员函数内部调用 CMultiLock 对象的构造函数。 然后调用 Lock 成员函数以确定资源是否可用(有信号)。 如果有资源可用,请继续执行成员函数的其余部分。 如果没有资源可用,请等待指定的时间以便系统释放资源,或返回失败。 不再需要使用资源后,如果要再次使用 CMultiLock 对象,则调用 Unlock 函数,否则允许销毁 CMultiLock 对象。
当线程具有大量它可以响应的 CEvent 对象时,CMultiLock 对象最有用。 创建包含所有 CEvent 指针的数组,并调用 Lock。 这将导致线程一直等到其中一个事件有信号。
cpp
DWORD Lock(
DWORD dwTimeOut = INFINITE,
BOOL bWaitForAll = TRUE,
DWORD dwWakeMask = 0);
返回值:
如果 Lock 失败,则返回 - 1。 如果成功,则返回下列值之一:
-
介于 WAIT_OBJECT_0 和 WAIT_OBJECT_0 + (对象数 - 1) 之间
如果 bWaitForAll 为 TRUE,则所有对象有信号(可用)。 如果 bWaitForAll 为 FALSE,则返回值 WAIT_OBJECT_0 是有信号(可用)对象数组中的索引。
-
WAIT_OBJECT_0 + (对象数)
dwWakeMask 中指定的事件在线程的输入队列中可用。
-
介于 WAIT_ABANDONED_0 和 WAIT_ABANDONED_0 + (对象数 - 1) 之间
如果 bWaitForAll 为 TRUE,则所有对象有信号,并且至少有一个对象是放弃的互斥对象。 如果 bWaitForAll 为 FALSE,则返回值 WAIT_ABANDONED_0 是满足等待的已放弃互斥对象数组中的索引。
-
WAIT_TIMEOUT
dwTimeOut 中指定的超时间隔在等待还未成功的情况下已过期。
步骤
1)定义全局变量。
CMutex mutex;
CSingleLock singleLock(&mutex); 或 CMultiLock multiLock(&mutex);
2)操作共享资源前,用singleLock.Lock() 或 multiLock.Lock()锁定,防止其他线程访问。
3)操作共享资源后,用singleLock.Unlock() 或 multiLock.Unlock() 解锁,让其他线程可以访问。
范例
修改上一节范例


cpp
//CCriticalSection criticalSection;//定义全局变量临界区
CMutex mutex;
CSingleLock singleLock(&mutex);
....
UINT WriteA(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
//criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
singleLock.Lock();//访问前锁定,其他线程使用Lock()时会等待,直到执行UnLock()
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'A';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
//criticalSection.Unlock();
singleLock.Unlock();
return 0;
}
UINT WriteB(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
//criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
singleLock.Lock();//访问前锁定,其他线程使用Lock()时会等待,直到执行UnLock()
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'B';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
//criticalSection.Unlock();
singleLock.Unlock();
return 0;
}

信号量
CSemaphore 类
类 CSemaphore 的对象表示"信号灯"。 信号灯是一个同步对象,用于控制对共享资源的访问并防止争用条件。
计数大于0允许访问,否则等待。
在访问仅支持有限数量用户的共享资源时,可使用信号灯很好地对其进行控制。
对象的当前计数 CSemaphore 是允许的其他用户数。 当计数达到零时,将插入对象控制 CSemaphore 的资源的所有尝试都会插入系统队列中,并等到超时或计数升至 0 以上。
若要使用 CSemaphore 对象,请根据需要构造 CSemaphore 对象。在资源的访问成员函数中创建 CSingleLock 类型或 CMultiLock 类型的变量。 然后调用锁对象的 Lock 成员函数(例如 CSingleLock::Lock)。 此时,线程将获取对资源的访问权限,等待资源释放并获取访问权限,或等待资源释放并超时而无法访问资源。 在任何情况下,资源都以线程安全的方式访问。 若要释放资源,请使用锁对象的 Unlock 成员函数(例如,CSingleLock::Unlock),或允许锁对象脱离范围。
步骤
1)定义全局变量。
CSemaphore semaphore(3, 3); //(初始计数, 最大计数)
2)操作共享资源前,用semaphore.Lock() ,此时计数-1。
3)操作共享资源后,用semaphore.Unlock() ,此时计数+1。
范例
修改上一节范例








cpp
//CCriticalSection criticalSection;//定义全局变量临界区
//CMutex mutex;
//CSingleLock singleLock(&mutex);
CSemaphore semaphore(2 ,2);
char16_t g_Array[10];//共享字符串组
....
UINT WriteA(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
//criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
//singleLock.Lock();//访问前锁定,其他线程使用Lock()时会等待,直到执行UnLock()
semaphore.Lock();
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'A';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
//criticalSection.Unlock();
//singleLock.Unlock();
semaphore.Unlock();
return 0;
}
UINT WriteB(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
//criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
//singleLock.Lock();//访问前锁定,其他线程使用Lock()时会等待,直到执行UnLock()
semaphore.Lock();
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'B';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
//criticalSection.Unlock();
//singleLock.Unlock();
semaphore.Unlock();
return 0;
}
UINT WriteC(LPVOID param)
{
CEdit* pEdit = (CEdit*)param;
pEdit->SetWindowTextW(L"");//果指定的窗口是控件,则控件的文本将更改。
//criticalSection.Lock();//访问前锁定临界区,其他线程使用Lock()时会等待,直到执行UnLock()
//singleLock.Lock();//访问前锁定,其他线程使用Lock()时会等待,直到执行UnLock()
semaphore.Lock();
for (size_t i = 0; i < 10; i++)
{
g_Array[i] = 'C';
pEdit->SetWindowTextW((LPCTSTR)g_Array);
Sleep(1000);
}
//criticalSection.Unlock();
//singleLock.Unlock();
semaphore.Unlock();
return 0;
}
....
void CMFCAPPDlg::OnBnClickedButton3()
{
AfxBeginThread(WriteA, &m_CtrlA);
AfxBeginThread(WriteC, &m_CtrlC);
AfxBeginThread(WriteB, &m_CtrlB);
}

事件
CEvent 类
表示一个事件,即支持一个线程向另一线程通知事件已发生的同步对象。
| 名称 | 描述 |
|---|---|
| CEvent::PulseEvent | 将事件设置为可用(已发出信号),释放等待线程,然后将事件设置为不可用(未发出信号)。 |
| CEvent::ResetEvent | 将事件设置为不可用(未发出信号)。 |
| CEvent::SetEvent | 将事件设置为可用(已发出信号)并释放所有等待线程。 |
| CEvent::Unlock | 释放事件对象。 |
每一个CEvent对象都有2个状态:有信号状态(Signaled)和无信号状态(NonSignaled)。
CEvent 对象有两种类型:手动和自动。
自动 CEvent 对象会在被至少一个线程后释放后,自动返回到未发出信号(不可用)状态。 默认情况下,除非在构造期间为 bManualReset 参数传递 TRUE,否则 CEvent 对象是自动的。
手动 CEvent 对象保持 SetEvent 或 ResetEvent 设置的状态,直到调用另一个函数。 若要创建手动 CEvent 对象,请在构造期间为 bManualReset 参数传递 TRUE。
若要使用 CEvent 对象,请根据需要构造 CEvent 对象。 指定要等待的事件的名称,并指定应用程序最初应该拥有它。 然后可以在构造函数返回时访问该事件。 调用 SetEvent 以发出事件对象的信号(使其可用),然后在访问完受控资源后调用 Unlock。
使用 CEvent 对象的替代方法是将 CEvent 类型的变量作为数据成员添加到要控制的类。 在构造受控对象的过程中,调用 CEvent 数据成员的构造函数并指定是否初始发出事件的信号,另外,指定所需的事件对象类型、事件的名称(如果要跨进程边界使用它),以及所需的任何安全属性。
若要以这种方式访问 CEvent 对象控制的资源,请先在资源的访问方法中创建类型为 CSingleLock 或 CMultiLock 的变量。 然后调用 lock 对象的 Lock 方法(例如 CMultiLock::Lock)。 此时,线程将获取对资源的访问权限,等待资源释放并获取访问权限,或等待资源释放并超时而无法访问资源。 在任何情况下,都是以线程安全的方式访问资源。 若要释放资源,请调用 SetEvent 以发出事件对象的信号,然后使用 lock 对象的 Unlock 方法(例如 CMultiLock::Unlock),或者让 lock 对象超出范围。
cpp
CEvent(
BOOL bInitiallyOwn = FALSE,
BOOL bManualReset = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);
bInitiallyOwn
如果为 TRUE,则启用 CMultilock 或 CSingleLock 对象的线程。 否则,所有想要访问资源的线程都必须等待。
bManualReset
如果为 TRUE,则指定事件对象是手动事件,否则事件对象是自动事件。
lpszName
CEvent 对象的名称。 如果对象将跨进程边界使用,则必须提供。 如果名称与现有事件匹配,构造函数将生成一个新的 CEvent 对象,该对象引用该名称的事件。 如果名称与不是事件的现有同步对象匹配,则构造将会失败。 如果为 NULL,则名称为 null。
lpsaAttribute
事件对象的安全属性。 有关此结构的完整说明,请参阅 Windows SDK 中的 SECURITY_ATTRIBUTES。
CSyncObject 类
一个纯虚拟类,提供 Win32 中的同步对象所共有的功能。
| 名称 | 描述 |
|---|---|
| CSyncObject::Lock | 获取对同步对象的访问权限。 |
| CSyncObject::Unlock | 获取对同步对象的访问权限。 |
Microsoft 基础类库提供了几个派生自 CSyncObject 的类。 这些是 CEvent、CMutex、CCriticalSection 和 CSemaphore。
范例一
cpp
//下面演示了CEvent类的简单用法。创建CEvent对象并将其作为参数传递给另一个线程。
//另一个线程将等待事件发出信号,然后退出
UINT __cdecl MyThreadProc(LPVOID lpParameter)
{
CEvent *pEvent = (CEvent *)(lpParameter); //转化传递的参数
VERIFY(pEvent != NULL);
//三、等待事件发出信号
::WaitForSingleObject(pEvent->m_hObject, INFINITE);
//五、终止线程,自动返回无信号状态
::AfxEndThread(0, FALSE);
return 0L;
}
void CEvent_Test()
{
//一、创建将传递给线程例程的CEvent对象
CEvent *pEvent = new CEvent(FALSE, FALSE);
//二、创建一个等待事件的线程
CWinThread *pThread;
pThread = ::AfxBeginThread(&MyThreadProc, pEvent, 0, 0, CREATE_SUSPENDED, NULL);//启动工作线程
pThread->m_bAutoDelete = FALSE;//指定是否在线程终止时销毁对象。
pThread->ResumeThread();//恢复线程
//四、通知线程执行下一个工作项
pEvent->SetEvent();
//六、等待线程消耗事件并返回
::WaitForSingleObject(pThread->m_hThread, INFINITE);
delete pThread;
delete pEvent;
}
范例二
cpp
//这个例子建立在前一个例子的基础上。
//创建第二个线程来计算素数。主线程将通知第二个线程计算序列中的下一个素数。
//在计算出每个数字后,第二线程向第一线程发出信号。
//最后,经过几次迭代后,工作线程会收到终止的信号。
class CPrimeTest
{
public:
CPrimeTest()
: m_pCalcNext(new CEvent(FALSE, FALSE)), //一、创建将传递给线程例程的CEvent对象
m_pCalcFinished(new CEvent(FALSE, FALSE)),//第二个FALSE为自动事件
m_pTerminateThread(new CEvent(FALSE, FALSE)),
m_iCurrentPrime(0)
{
//二、创建一个线程来计算素数
CWinThread *pThread;
pThread = ::AfxBeginThread(&PrimeCalcProc, this, 0, 0, CREATE_SUSPENDED, NULL);//线程挂起
pThread->m_bAutoDelete = FALSE;
pThread->ResumeThread();//恢复线程
//计算线程上序列的前10个素数
for (UINT i = 0; i < 10; i++)
{
//四、通知线程执行下一个工作项
m_pCalcNext->SetEvent();
//五、等待线程完成当前任务
::WaitForSingleObject(m_pCalcFinished->m_hObject, INFINITE);
//打印结果
TRACE(_T("The value of m_iCurrentPrime is: %d\n"), m_iCurrentPrime);
}
//七、通知工作线程退出并等待其完成
m_pTerminateThread->SetEvent();//跳过 while (MultiLock.Lock(INFINITE, FALSE) == WAIT_OBJECT_0)
//八、终止线程pThread
::WaitForSingleObject(pThread->m_hThread, INFINITE);
delete pThread;
}
~CPrimeTest()
{
delete m_pCalcNext;
delete m_pCalcFinished;
delete m_pTerminateThread;
}
private:
//确定给定的数字是否为素数
static BOOL IsPrime(INT ThisPrime)
{
if (ThisPrime < 2)
return FALSE;
for (INT n = 2; n < ThisPrime; n++)
{
if (ThisPrime % n == 0)
return FALSE;
}
return TRUE;
}
//计算序列中的下一个素数
static INT NextPrime(INT ThisPrime)
{
while (TRUE)
{
if (IsPrime(++ThisPrime))
{
return ThisPrime;
}
}
}
//工作线程负责计算序列中的下一个素数
static UINT __cdecl PrimeCalcProc(LPVOID lpParameter)
{
CPrimeTest *pThis = static_cast<CPrimeTest *>(lpParameter);//转化传递参数
VERIFY(pThis != NULL);
VERIFY(pThis->m_pCalcNext != NULL);
VERIFY(pThis->m_pCalcFinished != NULL);
VERIFY(pThis->m_pTerminateThread != NULL);
CSyncObject *pWaitObjects[] = {pThis->m_pCalcNext, pThis->m_pTerminateThread};
CMultiLock MultiLock(pWaitObjects, 2L);
//三、创建CMultiLock对象以等待各种事件,FALSE,当任一等待的对象有信号时,Lock 将返回。
//WAIT_OBJECT_0表示数组中的第一个事件,WAIT_OBJECT_0+1表示第二个
while (MultiLock.Lock(INFINITE, FALSE) == WAIT_OBJECT_0)
{
// 计算下一个素数
pThis->m_iCurrentPrime = NextPrime(pThis->m_iCurrentPrime);
//六、通知主线程计算已完成
pThis->m_pCalcFinished->SetEvent();
}
//九、终止线程
::AfxEndThread(0, FALSE);
return 0L;
}
CEvent *m_pCalcNext; //通知工作线程计算下一个素数
CEvent *m_pCalcFinished; //通知主线程电流计算已完成
CEvent *m_pTerminateThread; //通知工作线程终止
INT m_iCurrentPrime; //当前计算素数
};
范例三
给予下面链接修改
https://blog.csdn.net/hd51cc/article/details/155133568?spm=1011.2415.3001.5331


cpp
CEvent _event(false, true);
...
void CMFCDlg::OnBnClickedPrime()
{
//HWND hWnd = GetSafeHwnd();//获取窗口句柄
//AfxBeginThread(CalcPrime, hWnd);
AfxBeginThread(CalcPrime, 0);
_event.ResetEvent();//复位Event信号
_event.Lock();//等待SetEvent()
CString s;
s.Format(L"当前个数:%d .", n);
AfxMessageBox(s);
_event.Unlock();//取消等待
}
UINT CalcPrime(LPVOID param)
{
n = 0;
long j, k;
for (long i = 1; i <= 100000; i += 2)
{
k = (long)sqrt(i);
for (j = 2; j <= k; j++)
{
if (i % j == 0)
{
break;
}
}
if (j >= k + 1)
{
n += 1;
}
}
//::PostMessage((HWND)param, WM_CALCULATE, n, 0);//向主线程发送消息WM_CALCULATE
_event.SetEvent();//置位Event信号
return 0;
}
