[线程]--线程之间的同步

来源:互联网 发布:linux下chromium加速 编辑:程序博客网 时间:2024/05/16 10:01
线程之间的同步
2007-02-08 19:41

前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。这里主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。

1.临界区
  临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以抢占这个临界区,以便访问共享的数据。

    定义临界区变量
CRITICAL_SECTION gCriticalSection; 
  通常情况下,CRITICAL_SECTION结构体应该被定义为全局变量,以便于进程中的所有线程方便地按照变量名来引用该结构体。

  初始化临界区
VOID WINAPI InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向程序员定义的CRITICAL_SECTION变量
); 

  该函数用于对pcs所指的CRITICAL_SECTION结构体进行初始化。该函数只是设置了一些成员变量,它的运行一般不会失败,因此它采用了VOID类型的返回值。该函数必须在任何线程调用EnterCriticalSection函数之前被调用,如果一个线程试图进入一个未初始化的CRTICAL_SECTION,那么结果将是很难预计的。

  删除临界区
VOID WINAPI DeleteCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个不再需要的CRITICAL_SECTION变量
); 

  进入临界区
VOID WINAPI EnterCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将锁定的CRITICAL_SECTION变量
); 

  离开临界区
VOID WINAPI LeaveCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一个你即将离开的CRITICAL_SECTION变量
); 

  使用临界区编程的一般方法是:
void UpdateData()
{
 EnterCriticalSection(&gCriticalSection);
 ...//do something
 LeaveCriticalSection(&gCriticalSection);


  关于临界区的使用,有下列注意点:
  (1)每个共享资源使用一个CRITICAL_SECTION变量;
  (2)不要长时间运行关键代码段,当一个关键代码段长时间运行时,其他线程就会进入等待状态,这会降低应用程序的运行性能;
  (3)如果需要同时访问多个资源,则可能连续调用EnterCriticalSection;
  (4)Critical Section不是OS核心对象,如果进入临界区的线程"挂"了,将无法释放临界资源。这个缺点在Mutex中得到了弥补。



2.互斥

  互斥量的作用是保证每次只能有一个线程获得互斥量而得以继续执行,
使用CreateMutex函数创建: 
HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
 // 安全属性结构指针,可为NULL
 BOOL bInitialOwner, 
 //是否占有该互斥量,TRUE:占有,FALSE:不占有
  //后面的程序会讨论这个占有或者不占有的问题
 LPCTSTR lpName 
 //信号量的名称
);

  Mutex是核心对象,可以跨进程访问,下面的代码给出了从另一进程访问命名Mutex的例子:
HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_Access, FALSE, L"mutexName"); 
if (hMutex){
 … 

else{
 …


  相关API:
BOOL WINAPI ReleaseMutex(
 HANDLE hMutex
); 

  使用互斥编程的一般方法是:
void UpdateResource()
{
 WaitForSingleObject(hMutex,…);
 ...//do something
 ReleaseMutex(hMutex);




    下面来说说互斥和临界区的区别,互斥(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。互斥对象的行为特性与临界区相同,但是互斥对象属于内核对象,而临界区则属于用户方式对象,因此这导致mutex与Critical Section的如下不同:
  (1)互斥对象的运行速度比关键代码段要慢;
  (2)不同进程中的多个线程能够访问单个互斥对象;
  (3)线程在等待访问资源时可以设定一个超时值。


下表对两者做详细的对比,
                               互斥                                   临界区
运行速度                        慢                                      快
是否能跨进程                    是                                      否
声明                        HANDLE hmtx;                         CRITICAL_SECTION cs;
初始化               hmtx = CreateMutex(0, FALSE, 0);      InitializeCriticalSection(&cs);
清除                       CloseHandle(hmtx);                  DeleteCriticalSection(&cs);
无限等待           WaitForSingleObject(hmtx, INFINITE);        EnterCriticalSection(&cs);
0等待                   WaitForSingleObject(hmtx, 0);         TryEnterCriticalSection(&cs);
任意等待         WaitForSingleObject(hmtx, dwMillisecond);             不能
释放                      ReleaseMutex(hmtx);                   LeaveCriticalSection(&cs);  

3.信号量
    信号量(Semaphore)是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。
    举个通俗的例子来说明一下,我们来看看一个停车场是怎样运作的。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这是如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。 
   在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。 
   更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
  信号量的特点和用途可用下列几句话定义:
  (1)如果当前资源的数量大于0,则信号量有效;
  (2)如果当前资源数量是0,则信号量无效;
  (3)系统不允许当前资源的数量为负值;
  (4)当前资源数量不能大于最大资源数量。
  创建信号量
HANDLE CreateSemaphore (
 PSECURITY_ATTRIBUTE psa,
 LONG lInitialCount, //开始时可供使用的资源数
 LONG lMaximumCount, //最大资源数
PCTSTR pszName); 

  释放信号量
  通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增,该函数原型为:
BOOL WINAPI ReleaseSemaphore(
 HANDLE hSemaphore,
 LONG lReleaseCount, //信号量的当前资源数增加lReleaseCount
 LPLONG lpPreviousCount
); 

打开信号量
  和其他核心对象一样,信号量也可以通过名字跨进程访问,打开信号量的API为:
HANDLE OpenSemaphore (
 DWORD fdwAccess,
 BOOL bInherithandle,
 PCTSTR pszName
);