第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)的更多相关文章
- windows核心编程-互斥器(Mutexes)
线程同步的方式主要有:临界区.互斥区.事件.信号量四种方式. 前边讲过了临界区线程同步-----windows核心编程-关键段(临界区)线程同步,这章我来介绍一下互斥器(Mutexes)在线程同步中的 ...
- 第4章 同步控制 Synchronization ----critical section 互斥区 ,临界区
本章讨论 Win32 同步机制,并特别把重点放在多任务环境的效率上.撰写多线程程序的一个最具挑战性的问题就是:如何让一个线程和另一个线程合作.除非你让它们同心协力,否则必然会出现如第2章所说的&quo ...
- 第4章 同步控制 Synchronization ----事件(Event Objects)
Win32 中最具弹性的同步机制就属 events 对象了.Event 对象是一种核心对象,它的唯一目的就是成为激发状态或未激发状态.这两种状态全由程序来控制,不会成为 Wait...() 函数的副作 ...
- 第4章 同步控制 Synchronization ----同步机制的摘要
同步机制摘要Critical Section Critical section(临界区)用来实现"排他性占有".适用范围是单一进程的各线程之间.它是: 一个局部性对象,不是一个核 ...
- 第4章 同步控制 Synchronization ----信号量(Semaphore)
许多文件中都会提到 semaphores(信号量),因为在电脑科学中它是最具历史的同步机制.它可以让你陷入理论的泥淖之中,教授们则喜欢问你一些有关于信号量的疑难杂 症.你可能不容易找到一些关于 sem ...
- 第4章 同步控制 Synchronization ---哲学家进餐问题(The Dining Philosophers)
哲学家进餐问题是这样子的:好几位哲学家围绕着餐桌坐,每一位哲学家要么思考,要么等待,要么就吃饭.为了吃饭,哲学家必须拿起两支筷子(分放于左右两端).不幸的是,筷子的数量和哲学家相等,所以每支筷子必须由 ...
- 第4章 同步控制 Synchronization ----Interlocked Variables
同步机制的最简单类型是使用 interlocked 函数,对着标准的 32 位变量进行操作.这些函数并没有提供"等待"机能,它们只是保证对某个特定变量的存取操作是"一个一 ...
- 第4章 同步控制 Synchronization ----死锁(DeadLock)
Jeffrey Richter 在他所主持的 Win32 Q&A 专栏(Microsoft Systems Journal,1996/07)中曾经提到过,Windows NT 和 Window ...
- java JDK8 学习笔记——第17章 反射与类加载器
第十七章 反射与类加载器 17.1 运用反射 反射:.class文档反映了类基本信息,从Class等API取得类信息的方式称为反射. 17.1.1 Class与.class文档 1.java.lang ...
随机推荐
- uval 6657 GCD XOR
GCD XORGiven an integer N, nd how many pairs (A; B) are there such that: gcd(A; B) = A xor B where1 ...
- Servlet知识点
如果请求采用Get方式,则重写doGet()方法,如果请求采用Post方式,则重写doPost()方法. 下面是重写doGet()方法的servlet例子. servlet继承如下类: 整体结构: 在 ...
- Python内存优化
实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...
- C# 导出数据到Excel模板中(转)
今天做报表的时候遇到了多表头的问题,而且相应的报表的格式都一样.所以就采用了报表模板的方式来进行. 第一步:在开发的当前项目中引入:Microsoft.Office.Interop.Excel:Sys ...
- 排序--SelectionSort 选择排序
选择排序 no implementation 选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理是每一次从待排序的元素中中选出最小(或最大)的一个元素,存放在序列的起始位置 ...
- [转载]GIF、JPEG 和 PNG的区别在哪里?
原文地址:GIF.JPEG 和 PNG的区别在哪里?作者:苗得雨 GIF.JPEG 和 PNG 是三种最常见的图片格式. GIF:1987 年诞生,常用于网页动画,使用无损压缩,支持 256 种颜色( ...
- 团队作业4——第一次项目冲刺 SeCOnd DaY
项目冲刺--Double Kill 喂喂喂,你好你好,听得见吗?这里是天霸动霸.tua广播站,我是主播小学生¥-¥ 第一次敏捷冲刺平稳的度过了第一天,第一天的任务大家也圆满完成啦[拍手庆祝],那么今天 ...
- 201521123029《Java程序设计》第八周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 1.2 选做:收集你认为有用的代码片段 答: 2. 书面作业 本次作业题集集合 1.List中指定元素的删除( ...
- 201521123065 《Java程序设计》第5周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 1.2 可选:使用常规方法总结其他上课内容. 1.ArrayList只能存放对象: 2.对象包装类之间使用equals进行比较 ...
- 201521123095 《Java程序设计》第5周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 2. 书面作业 1.1 com.parent包中Child.java文件能否编译通过?哪句会出现错误?试改正该错误.并分析输出 ...