第4章 同步控制 Synchronization ----互斥器(Mutexes)

Posted 我是张洪铭我是熊博士

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第4章 同步控制 Synchronization ----互斥器(Mutexes)相关的知识,希望对你有一定的参考价值。

Win32 的 Mutex 用途和 critical section 非常类似,但是它牺牲速度以增加弹性。或许你已经猜到了,mutex 是 MUTual EXclusion 的缩写。一个时间内只能够有一个线程拥有 mutex,就好像同一时间内只能够有一个线程进入同一个 critical section 一样。
虽然 mutex 和 critical section 做相同的事情,但是它们的运作还是有差别的:

      锁住一个未被拥有的 mutex,比锁住一个未被拥有的 critical section,需要花费几乎 100 倍的时间。因为 critical section 不需要进入操作系统核心,直接在“user mode”就可以进行操作。(译注:作者这里所谓的“usermode”,是相对于 Windows NT 的“kernel mode”而言。至于 Windows 95 底下,没有所谓“user mode”这个名词或观念,应该是指 ring3 层次。)
   Mutexes 可以跨进程使用。Critical section 则只能够在同一个进程中使用。
  等待一个 mutex 时,你可以指定“结束等待”的时间长度。但对于critical section 则不行。
  以下是两种对象的相关函数比较:

CRITICAL_SECTION                Mutex 核心对象
InitializeCriticalSection()              CreateMutex()
                              OpenMutex()

EnterCriticalSection()                 WaitForSingleObject()
                              WaitForMultipleObjects()
                              MsgWaitForMultipleObjects()

LeaveCriticalSection()               ReleaseMutex()
DeleteCriticalSection()               CloseHandle()

 

为了能够跨进程使用同一个 mutex,你可以在产生 mutex 时指定其名称。如果你指定了名称,系统中的其他任何线程就可以使用这个名称来处理 mutex。一定要使用名称,因为你没有办法把 handle 交给一个执行中的进程。
记住,其他程序也可能使用这个同步机制,所以 mutex 名称对整个系统而言是全局性的。不要把你的 mutex 对象命名为“Object”或“Mutex”之类,那太过普遍。请使用一些独一无二的名称,如公司名称或应用程序名称等等。

产生一个互斥器(Mutex)

与critical sections 不同,当你产生一个 mutex 时,你有某些选择空间。Mutex 是一个核心对象,因此它被保持在系统核心之中,并且和其他核心对象一样,有所谓的引用计数(reference count)。虽然 mutex 的机能与 critical section 十分类似,但由于 Win32 术语带来的迷惑,mutex 可能不大容易了解。
你可以利用 CreateMutex() 产生一个 mutex:
HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner,
    LPCTSTR lpName
);
参数
    lpMutexAttributes     安全属性。NULL 表示使用默认的属性。这一指定在 Windows 95 中无效。
    bInitialOwner         如果你希望“调用 CreateMutex() 的这个线程”拥有产生出来的 mutex,就将此值设为 TRUE。
    lpName             mutex 的名称(一个字符串)。任何进程或线程都可以根据此名称使用这一 mutex。名称可以是任意字符串,只要不含反斜线(backslash,\)即可。
返回值
    如果成功,则传回一个 handle,否则传回 NULL。调用 GetLastError() 可以获得更进一步的信息。如果指定的 mutex 名称已经存在,GetLastError() 会传回 ERROR_ALREADY_EXISTS。

当你不再需要一个 mutex 时,你可以调用 CloseHandle() 将它关闭。和其他核心对象一样,mutex 有一个引用计数(reference count)。每次你调用CloseHandle(),引用计数便减 1。当引用计数达到0 时,mutex 便自动被系统清除掉。下面这个 CreateAndDeleteMutex() 函数会先产生一个 mutex 然后把它删除。这个 mutex 没有安全属性,不属于现行线程,名为“Demonstration Mutex”。
HANDLE hMutex;
void CreateAndDeleteMutex()
{
    hMutex = CreateMutex(NULL, FALSE, "Demonstration Mutex");
        /* Do something here */
    CloseHandle(hMutex);
}

打开一个互斥器(Mutex)

  如果 mutex 已经被产生了,并有一个名称,那么任何其他的进程和线程便可以根据该名称打开那个 mutex(我这里并不考虑安全属性)。
  如果你调用 CreateMutex() 并指定一个早已存在的 mutex 名称,Win32会回给你一个 mutex handle,而不会为你产生一个新的 mutex。就像上面所说的,GetLastError() 会传回 ERROR_ALREADY_EXISTS。
你也可以使用 OpenMutex() 打开(而非产生)一个原已存在的 mutex。这种情况通常是因为,你写了一个 client 进程,并与同一台机器上的 server 进程交谈,而只有 server 进程才应该产生 mutex,因为它保护了 server 所定义的结构体。
关于 OpenMutex(),请参阅 Win32 Programmer‘s Reference。你也可以在
Visual C++ 的联机帮助文件中找到相关资料。

 

锁住一个互斥器(Mutex)

  欲获得一个 mutex 的拥有权,请使用 Win32 的 Wait...() 函数。Wait...()对 mutex 所做的事情和 EnterCriticalSection() 对 critical section 所做的事情差不多,倒是一大堆术语容易把你迷惑了。
一旦没有任何线程拥有 mutex,这个 mutex 便处于激发状态。因此,如果没有任何线程拥有那个 mutex,Wait...() 便会成功。反过来说,当线程拥有mutex 时,它便不处于激发状态。如果有某个线程正在等待一个未被激发的mutex,它便将进入“blocking”(阻塞)状态。也就是说,该线程会停止执行,直到 mutex 被其拥有者释放并处于激发状态。

下面是某种情节的发展:
    1. 我们有一个 mutex,此时没有任何线程拥有它,也就是说,它处于非激发状态(译注)。
    2. 某个线程调用 WaitForSingleObject()(或任何其他的 Wait...() 函数),并指定该 mutex handle 为参数。
    3. Win32 于是将该 mutex 的拥有权给予这个线程,然后将此 mutex 的状态短暂地设为激发状态,于是 Wait...() 函数返回。
    4. Mutex 立刻又被设定为非激发状态,使任何处于等待状态下的其他线程没有办法获得其拥有权。
    5. 获得该 mutex 之线程调用 ReleaseMutex(),将 mutex 释放掉。于是循环回到第一场景,周而复始。

  译注 我想你很容易被作者的上一段文字迷惑,因为它的第一点和更前一段文字中的“一旦没有任何线程拥有 mutex,这个 mutex 便处于激发状态”有点背道而驰。基本上,或许更精密地说,所谓的“mutex 激发状态”应该是:当没有任何线程拥有该 mutex 而且有一个线程正以 Wait...() 等待该 mutex,该mutex 就会短暂地出现激发状态,使 Wait...() 得以返回。
ReleaseMutex() 的规格如下:
BOOL ReleaseMutex(
    HANDLE hMutex
);
参数
    hMutex         欲释放之 mutex 的 handle。
返回值
    如果成功,传回 TRUE。如果失败,传回 FALSE。

Mutex 的拥有权是第二个容易引人迷惑的地方。Mutex 的拥有权并非属于那个产生它的线程,而是那个最后对此 mutex 进行 Wait...() 操作并且尚未进行 ReleaseMutex() 操作的线程。线程拥有 mutex 就好像线程进入 critical section 一样。一次只能有一个线程拥有该 mutex。
Mutex 的被摧毁和其拥有权没有什么关系。和大部分其他的核心对象一样,mutex 是在其引用计数降为 0 时被操作系统摧毁的。每当线程对此 mutex 调用一次 CloseHandle(),或是当线程结束时,mutex 的引用计数即下降 1。
如果拥有某 mutex 之线程结束了,该 mutex 会被自动清除的唯一情况是:此线程是最后一个与该 mutex handle 有关联的线程。否则此核心对象的引用计数仍然是比 0 大,其他线程(以及进程)仍然可以拥有此 mutex 的合法handle。然而,当线程结束而没有释放某个 mutex 时,有一种特殊的处理方式。

处理被舍弃的互斥器(Mutexes)

  在一个适当的程序中,线程绝对不应该在它即将结束前还拥有一个mutex,因为这意味着线程没有能够适当地清除其资源。不幸地是,我们并不身处在一个完美的世界,有时候,因为某种理由,线程可能没有在结束前调用ReleaseMutex()。为了解决这个问题,mutex 有一个非常重要的特性。这性质在各种同步机制中是独一无二的。如果线程拥有一个 mutex 而在结束前没有调用 ReleaseMutex(),mutex 不会被摧毁。取而代之的是,该 mutex 会被视为“ 未被拥有” 以及“ 未被激发” , 而下一个等待中的线程会被以WAIT_ABANDONED_0 通知。不论线程是因为 ExitThread() 而结束,或是因当掉而结束,这种情况都存在。
  如果其他线程正以 WaitForMultipleObjects() 等待此 mutex,该函数也会返回,传回值介于 WAIT_ABANDONED_0 和 (WAIT_ABANDONED_0_n +1)之间,其中的 n 是指handle 数组的元素个数。线程可以根据这个值了解到究竟哪一个 mutex 被放弃了。至于 WaitForSingleObject() , 则只是传回WAIT_ABANDONED_0。
  “知道一个 mutex 被舍弃”是一件简单的事情,但要知道如何应对可就比较困难了。毕竟 mutex 是用来确保某些操作能够自动被进行的,如果线程死于半途,很有可能被保护的那些数据会受到无法修复的伤害。

让哲学家们进餐

  让我们回头重新看看哲学家进餐问题。在范例程序 DINING 中产生一组mutexes 用来表示那些筷子。产生 mutexes 的程序操作像这样:
      for (i=0; i<PHILOSOPHERS; i++)
          gChopStick[i] = CreateMutex(NULL, FALSE, NULL);
  CreateMutex() 的参数告诉我们,这些 mutexes 的安全属性采用缺省值,没有初始拥有者,也没有名称。每一支筷子有一个 mutex 对应之。我们之所以使用未具名的 mutexes,为的是筷子数组是全局数据,每一个线程都能够存取它。
  就像 critical sections 一样,mutexes 用来保护资源。在存取一个受保护的资源时,你的程序代码必须收到 mutex 的拥有权——藉由调用 Wait...() 函数获得。
  哲学家们可以使用 WaitForSingleObject() 来等待吃饭,但那可就像critical section 一样了(同时也带来相同的死锁问题)。他们也可以使用WaitForMultipleObjects() 来等待,于是可以修正因 EnterCriticalSection() 和WaitForSingleObject() 而造成的死锁问题。
  实际上我们是使用 WaitForMultipleObjects()来等待两支筷子。如果只有一支筷子可用,不算是取得一“双”筷子(WaitForMultipleObjects() 也因此不会返回)。程序代码像这样:
    WaitForMultipleObjects(2, myChopsticks, TRUE, INFINITE);
  这个函数的参数告诉我们,myChopsticks 数组中有两个 handles 是等待目标。当其中每一个 handle 都处于激发状态时,该函数才会返回。它会无穷尽地等待下去,没有时间限制。
  如果你以 WaitForMultipleObjects() 的方式执行 DINING 程序,你会发现哲学家们能够持续地吃,死锁永远不会发生。

修正SwapLists

  我们用于解决哲学家进餐问题的技术,也可以用来解决我们在 SwapLists()所遭遇的问题。任何时候只要你想锁住超过一个以上的同步对象,你就有死锁的潜在病因。如果总是在相同时间把所有对象都锁住,问题可去矣。列表4-2显示新版的 SwapLists()。

列表 4-2 使用WaitForMultipleObjects() 修正 SwapLists

#0001 struct Node
#0002 {
#0003         struct Node *next;
#0004         int data;
#0005 };
#0006
#0007 struct List
#0008 {
#0009         struct Node *head;
#0010         HANDLE hMutex;
#0011 };
#0012
#0013 struct List *CreateList()
#0014 {
#0015         List *list = (List *)malloc(sizeof(struct List));
#0016         list->head = NULL;
#0017         list->hMutex = CreateMutex(NULL, FALSE, NULL);
#0018         return list;
#0019 }
#0020
#0021 void DeleteList(struct List *list)
#0022 {
#0023         CloseHandle(list->hMutex);
#0024         free(list);
#0025 }
#0026
#0027 void SwapLists(struct List *list, struct List *list2)
#0028 {
#0029         struct List *tmp_list;
#0030         HANDLE arrhandles[2];
#0031
#0032         arrhandles[0] = list1->hMutex;
#0033         arrhandles[1] = list2->hMutex;
#0034         WaitForMultipleObjects(2, arrHandles, TRUE, INFINITE);
#0035         tmp_list = list1->head;
#0036         list1->head = list2->head;
#0037         list2->head = tmp_list;
#0038         ReleaseMutex(arrhandles[0]);
#0039         ReleaseMutex(arrhandles[1]);
#0040 }


为什么有一个最初拥有者?

  CreateMutex() 的第二个参数 bInitialOwner,允许你指定现行线程(current thread)是否立刻拥有即将产生出来的mutex。乍见之下这个参数或许只是提供一种方便性,但事实上它阻止了一种 race condition 的发生。
  与 critical section 不同,mutexes 可以跨进程使用,以及跨线程使用。Mutex 可以根据其名称而被开启。所以,另一个进程可以完全不需要和产生mutex 的进程打声招呼, 就根据名称开启一个 mutex 。如果没有bInitialOwner,你就必须写下这样的代码:
          HANDLE hMutex = CreateMutex(NULL, FALSE, "Sample Name");
          int result = WaitForSingleObject(hMutex, INFINITE);
  但是这样的安排可能会产生 race condition。如果在 CreateMutex 完成之后,发生了一个 context switch,执行权被切换到另一个线程,那么其他进程就有可能在 mutex 的产生者调用 WaitForSingleObject() 之前,锁住这个mutex 对象。

以上是关于第4章 同步控制 Synchronization ----互斥器(Mutexes)的主要内容,如果未能解决你的问题,请参考以下文章

第4章 控制执行流程

联想教育应用使用说明(7.6版本号)——第4章 网络控制工具的使用

第12章Cortex-M4-SPI-Bus

第6章-控制器-加速指令执行的方法

3.16第4次作业,第6章与第7章

读《Go并发编程实战》第4章 流程控制方式