使线程同步
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程同步是一个非常大的话题,包括方方面面的内容。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
临界区
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
代
码实现
清
单
附下:
//
临
界区
结
构
对
象
CRITICAL_SECTION g_cs; // 共享 资 源 char g_cArray[10]; UINT ThreadProc10(LPVOID pParam) { // 进 入 临 界区 EnterCriticalSection(&g_cs); // 对 共享 资 源 进 行写入操作 for (int i = 0; i < 10; i++) { g_cArray[i] = 'a'; Sleep(1); } // 离 开临 界区 LeaveCriticalSection(&g_cs); return 0; } UINT ThreadProc11(LPVOID pParam) { // 进 入 临 界区 EnterCriticalSection(&g_cs); // 对 共享 资 源 进 行写入操作 for (int i = 0; i < 10; i++) { g_cArray[10 - i - 1] = 'b'; Sleep(1); } // 离 开临 界区 LeaveCriticalSection(&g_cs); return 0; } …… void CSample08View::OnCriticalSection() { // 初始化 临 界区 InitializeCriticalSection(&g_cs); // 启 动线 程 AfxBeginThread(ThreadProc10, NULL); AfxBeginThread(ThreadProc11, NULL); // 等待 计 算完 毕 Sleep(300); // 报 告 计 算 结 果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); } |
在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定程度上影响。程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。如果进入了临界区却一直没有释放,同样也会引起其他线程的长时间等待。换句话说,在执行了EnterCriticalSection()语句进入临界区后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句的执行。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:
MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:
// MFC临界区类对象
CCriticalSection g_clsCriticalSection;
// 共享资源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
// 进入临界区
g_clsCriticalSection.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = 'a';
Sleep(1);
}
// 离开临界区
g_clsCriticalSection.Unlock();
return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
// 进入临界区
g_clsCriticalSection.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep(1);
}
// 离开临界区
g_clsCriticalSection.Unlock();
return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
// 启动线程
AfxBeginThread(ThreadProc20, NULL);
AfxBeginThread(ThreadProc21, NULL);
// 等待计算完毕
Sleep(300);
// 报告计算结果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
CCriticalSection g_clsCriticalSection;
// 共享资源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
// 进入临界区
g_clsCriticalSection.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = 'a';
Sleep(1);
}
// 离开临界区
g_clsCriticalSection.Unlock();
return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
// 进入临界区
g_clsCriticalSection.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep(1);
}
// 离开临界区
g_clsCriticalSection.Unlock();
return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
// 启动线程
AfxBeginThread(ThreadProc20, NULL);
AfxBeginThread(ThreadProc21, NULL);
// 等待计算完毕
Sleep(300);
// 报告计算结果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
管理事件内核对象
在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:
在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:
//
事件句柄
HANDLE hEvent = NULL; // 共享 资 源 char g_cArray[10]; …… UINT ThreadProc12(LPVOID pParam) { // 等待事件置位 WaitForSingleObject(hEvent, INFINITE); // 对 共享 资 源 进 行写入操作 for (int i = 0; i < 10; i++) { g_cArray[i] = 'a'; Sleep(1); } // 处 理完成后即将事件 对 象置位 SetEvent(hEvent); return 0; } UINT ThreadProc13(LPVOID pParam) { // 等待事件置位 WaitForSingleObject(hEvent, INFINITE); // 对 共享 资 源 进 行写入操作 for (int i = 0; i < 10; i++) { g_cArray[10 - i - 1] = 'b'; Sleep(1); } // 处 理完成后即将 事件 对 象置位 SetEvent(hEvent); return 0; } …… void CSample08View::OnEvent() { // 创 建事件 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // 事件置位 SetEvent(hEvent); // 启 动线 程 AfxBeginThread(ThreadProc12, NULL); AfxBeginThread(ThreadProc13, NULL); // 等待 计 算完 毕 Sleep(300); // 报 告 计 算 结 果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); } |
在 创 建 线 程前,首先 创 建一个可以自 动复 位的事件内核 对 象 hEvent ,而 线 程函数 则 通 过 WaitForSingleObject ()等待函数无限等待 hEvent 的置位,只有在事件置位 时 WaitForSingleObject ()才会返回,被保 护 的代 码 将得以 执 行。 对 于以自 动复 位方式 创 建的事件 对 象,在其置位后一被 WaitForSingleObject ()等待到就会立即 复 位,也就是 说 在 执 行 ThreadProc12 ()中的受保 护 代 码时 ,事件 对 象已 经 是 复 位状 态 的, 这时 即使有 ThreadProc13 () 对 CPU 的 抢 占,也会由于 WaitForSingleObject ()没有 hEvent 的置位而不能 继续执 行,也就没有可能破坏受保 护 的共享 资 源。在 ThreadProc12 ()中的 处 理完成后可以通 过 SetEvent () 对 hEvent 的置位而允 许 ThreadProc13 () 对 共享 资 源 g_cArray 的 处 理。 这 里 SetEvent ()所起的作用可以看作是 对 某 项 特定任 务 完成的通知 。
使用
临
界区只能同
步
同一
进
程中的
线
程,而使用事件内核
对
象
则
可以
对进
程外的
线
程
进
行同
步
,其前提是得到
对
此事件
对
象的
访问权
。
可以通
过
OpenEvent
()函数
获
取得到,其函数原型
为
:
HANDLE OpenEvent( DWORD dwDesiredAccess, // 访问标 志 BOOL bInheritHandle, // 继 承 标 志 LPCTSTR lpName // 指向事件 对 象名的指 针 ); |
如果事件 对 象已 创 建(在 创 建事件 时 需要指定事件名),函数将返回指定事件的句柄。 对 于那些在 创 建事件 时 没有指定事件名的事件内核 对 象,可以通 过 使用内核 对 象的 继 承性或是 调 用 DuplicateHandle ()函数来 调 用 CreateEvent ()以 获 得 对 指定事件 对 象的 访问权 。在 获 取到 访问权 后所 进 行的同 步 操作与在同一个 进 程中所 进 行的 线 程同 步 操作是一 样 的。
如果需要在一个 线 程中等待多个事件, 则 用 WaitForMultipleObjects ()来等待。 WaitForMultipleObjects ()与 WaitForSingleObject () 类 似,同 时监视 位于句柄数 组 中的所有句柄。 这 些被 监视对 象的句柄享有平等的 优 先 权 ,任何一个句柄都不可能比其他句柄具有更高的 优 先 权 。 WaitForMultipleObjects ()的函数原型 为 :
DWORD WaitForMultipleObjects( DWORD nCount, // 等待句柄数 CONST HANDLE *lpHandles, // 句柄数 组 首地址 BOOL fWaitAll, // 等待 标 志 DWORD dwMilliseconds // 等待 时间间 隔 ); |
参数 nCount 指定了要等待的内核 对 象的数目,存放 这 些内核 对 象的数 组 由 lpHandles 来指向。 fWaitAll 对 指定的 这 nCount 个内核 对 象的两 种 等待方式 进 行了指定, 为 TRUE 时 当所有 对 象都被通知 时 函数才会返回, 为 FALSE 则 只要其中任何一个得到通知就可以返回 。
WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。 下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。通过对两个事件内核对象的等待来控制线程任务的执行与中途退出:
// MFC事件类对象
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = 'a';
Sleep(1);
}
// 事件置位
g_clsEvent.SetEvent();
return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
// 等待事件
g_clsEvent.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep(1);
}
return 0;
}
……
void CSample08View::OnEventMfc()
{
// 启动线程
AfxBeginThread(ThreadProc22, NULL);
AfxBeginThread(ThreadProc23, NULL);
// 等待计算完毕
Sleep(300);
// 报告计算结果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = 'a';
Sleep(1);
}
// 事件置位
g_clsEvent.SetEvent();
return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
// 等待事件
g_clsEvent.Lock();
// 对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep(1);
}
return 0;
}
……
void CSample08View::OnEventMfc()
{
// 启动线程
AfxBeginThread(ThreadProc22, NULL);
AfxBeginThread(ThreadProc23, NULL);
// 等待计算完毕
Sleep(300);
// 报告计算结果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}