商务合同的内容:Visual C++线程同步技术剖析:临界区,时间,信号量,互斥量

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 20:02:23
摘要: 多线程同步技术是计算机软件开发的重要技术,本文对多线程的各种同步技术的原理和实现进行了初步探讨。

关键词: VC++6.0; 线程同步;临界区;事件;互斥;信号量;

正文

使线程同步

  在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。

  如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。

  为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。

  线程同步是一个非常大的话题,包括方方面面的内容。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。

  内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。

临界区

  临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

  临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。



图1 使用临界区保持线程同步

下面通过一段代码展示了临界区在保护多线程访问的共享资源中的作用。通过两个线程来分别对全局变量g_cArray[10]进行写入操作,用临界区结构对象g_cs来保持线程的同步,并在开启线程前对其进行初始化。为了使实验效果更加明显,体现出临界区的作用,在线程函数对共享资源g_cArray[10]的写入时,以Sleep()函数延迟1毫秒,使其他线程同其抢占CPU的可能性增大。如果不使用临界区对其进行保护,则共享资源数据将被破坏(参见图1(a)所示计算结果),而使用临界区对线程保持同步后则可以得到正确的结果(参见图1(b)所示计算结果)。代码实现清单附下:
// 临界区结构对象
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 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则只要其中任何一个得到通知就可以返回。dwMilliseconds在饫锏淖饔糜朐赪aitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均为已通知状态(当fWaitAll为TRUE时)或是用以减去WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。 下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。通过对两个事件内核对象的等待来控制线程任务的执行与中途退出:

// 存放事件句柄的数组
HANDLE hEvents[2];
UINT ThreadProc14(LPVOID pParam)
{
 // 等待开启事件
 DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
 // 如果开启事件到达则线程开始执行任务
 if (dwRet1 == WAIT_OBJECT_0)
 {
  AfxMessageBox("线程开始工作!");
  while (true)
  {
   for (int i = 0; i < 10000; i++);
   // 在任务处理过程中等待结束事件
   DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0);
   // 如果结束事件置位则立即终止任务的执行
   if (dwRet2 == WAIT_OBJECT_0 + 1)
    break;
  }
 }
 AfxMessageBox("线程退出!");
 return 0;
}
……
void CSample08View::OnStartEvent()
{
 // 创建线程
 for (int i = 0; i < 2; i++)
  hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
  // 开启线程
  AfxBeginThread(ThreadProc14, NULL);
  // 设置事件0(开启事件)
  SetEvent(hEvents[0]);
}
void CSample08View::OnEndevent()
{
 // 设置事件1(结束事件)
 SetEvent(hEvents[1]);
}



  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两线程的同步过程:



图2 CEvent类对线程的同步过程示意

B线程在执行到CEvent类成员函数Lock()时将会发生阻塞,而A线程此时则可以在没有B线程干扰的情况下对共享资源进行处理,并在处理完成后通过成员函数SetEvent()向B发出事件,使其被释放,得以对A先前已处理完毕的共享资源进行操作。可见,使用CEvent类对线程的同步方法与通过API函数进行线程同步的处理方法是基本一致的。前面的API处理代码可用CEvent类将其改写为:




// 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);
}





文献来源:http://www.yuanma.org/data/2006/1209/article_1923.htm