第4章 同步控制 Synchronization ----critical section 互斥区 ,临界区
本章讨论 Win32 同步机制,并特别把重点放在多任务环境的效率上。撰写多线程程序的一个最具挑战性的问题就是:如何让一个线程和另一个线程合作。除非你让它们同心协力,否则必然会出现如第2章所说的“raceconditions”(竞争条件)和“data corruption”(数据被破坏)的情况。
在典型的办公室文化中,协调工作是由管理者来执行的。类似的解决方案,也就是“让某个线程成为大家的老板”。当然可以在软件中实现出来,但是每逢它们需要指挥时,就要它们排队等候,其实有着严重的缺点。通常那会使得队伍又长又慢。这对于一个高效率的电算系统而言,实在不是一个有用的解决方案。
译注 让我先对同步(synchronous)与异步(asynchronous)做个说明。当程序1调用程序2时,程序1停下不动,直到程序2完成回到程序1来,程序1才继续下去,这就是所谓的“synchronous”。如果程序1调用程序2后,径自继续自己的下一个动作,那么两者之间就是所谓的“asynchronous”。Win32 API中的 SendMessage() 就是同步行为,而 PostMessage() 就是异步行为。如下图:
在 Windows 系统中,PostMessage() 是把消息放到对方的消息队列中,然后不管三七二十一,就回到原调用点继续执行,所以这是异步(asynchronous)行为。而 SendMessage() 根本就像是“直接调用窗口之窗口函数”,除非等该窗口函数结束,是不会回到原调用点的,所以它是同步(synchronous)行为。
Win32 中关于进程和线程的协调工作是由同步机制( synchronousmechanism)来完成的。同步机制相当于线程之间的红绿灯。你可以设计让一组线程使用同一个红绿灯系统。这个红绿灯系统负责给某个线程绿灯而给其他线程红灯。这一组红绿灯系统必须确保每一个线程都有机会获得绿灯。
有好多种同步机制可以运用。使用哪一种则完全视欲解决的问题而定。当我讨论每一种同步机制时,我会说明“何时”以及“为什么”应该使用它。
这些同步机制常常以各种方式组合在一起,以产生出更精密的机制。如果你把那些基本的同步机制视为建筑物的小件组块,你就能够设计出更适合你的特殊同步机制。
Critical Sections(关键区域、临界区域)
Win32 之中最容易使用的一个同步机制就是 critical sections。所谓critical sections 意指一小块“用来处理一份被共享之资源”的程序代码。这里所谓的资源,并不是指来自 .RES(资源文件)的 Windows 资源,而是广义地指一块内存、一个数据结构、一个文件,或任何其他具有“使用之排他性”的东西。也就是说,“资源”每一次(同一时间内)只能够被一个线程处理。
你可能必须在程序的许多地方处理这一块可共享的资源。所有这些程序代码可以被同一个 critical section 保护起来。为了阻止问题发生,一次只能有一个线程获准进入 critical section 中(相对地也就是说资源受到了保护)。实施的方式是在程序中加上“进入”或“离开”critical section 的操作。如果有一个线程已经“进入”某个 critical section,另一个线程就绝对不能够进入同一个 critical section。
在 Win32 程序中你可以为每一个需要保护的资源声明一个CRITICAL_SECTION 类型的变量。这个变量扮演红绿灯的角色,让同一时间内只有一个线程进入 critical section。
Critical section 并不是核心对象。因此,没有所谓 handle 这样的东西。它和核心对象不同,它存在于进程的内存空间中。你不需要使用像“Create”这样的 API 函数获得一个 critical section handle。你应该做的是将一个类型为CRITICAL_SECTION 的局部变量初始化, 方法是调用InitializeCriticalSection():
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection 一个指针, 指向欲被初始化的
CRITICAL_SECTION 变量。这个变量应该在你的程序中定义。
返回值
此函数传回 void。
当你用毕 critical section 时,你必须调用 DeleteCriticalSection() 清除它。这个函数并没有“释放对象”的意义在里头,不要把它和 C++ 的 delete 运算符混淆了。
VOID DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection 指向一个不再需要的 CRITICAL_SECTION 变量。
返回值
此函数传回 void。
下面就是一个基本的调用程序,用来产生并摧毁一个 critical section。请注意:gCriticalSection 被声明在程序最上方,作为任一线程都可以使用的全局变量。
CRITICAL_SECTION gCriticalSection;
void CreateDeleteCriticalSection()
{
InitializeCriticalSection(&gCriticalSection);
/* Do something here */
DeleteCriticalSection(&gCriticalSection);
}
一旦 critical section 被初始化,每一个线程就可以进入其中——只要它通过了 EnterCriticalSection() 这一关。
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection 指向一个你即将锁定的 CRITICAL_SECTION 变量。
返回值
此函数传回 void。
当线程准备好要离开 critical section 时,它必须调用LeaveCriticalSection():
VOID LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数
lpCriticalSection 指向一个你即将解除锁定的 CRITICAL_SECTION 变量。
返回值
此函数传回 void。
延续稍早我所举的例子,下面是使用我所产生之 critical section 的一个例子:
void UpdateData()
{
EnterCriticalSection(&gCriticalSection);
/* Update the resource */
LeaveCriticalSection(&gCriticalSection);
}
你可能会发现,有好几个函数都需要进入同一个 critical section(以上例而言指的就是 gCriticalSection)中,它们都前后包夹着 Enter/Leave 函数,并使用相同的参数。你应该在每一个存取全局数据的地方使用 Enter/Leave 函数。有时候 Enter/Leave 甚至会在同一个函数中出现数次——如果这个函数需要很长的运行时间。
不知道你是否还记得第1章那个被破坏的链表(linked list)例子,问题在于,像 insert 和 add 这样的操作应该避免同时发生。让我们看看如何使用critical sections 来阻止破坏的发生。每次处理链表,前后都必须包夹进入和离开 critical section 的操作。列表4-1 是一个实例。
列表4-1 链表,配合critical section:
#0001 typedef struct _Node
#0002 {
#0003 struct _Node *next;
#0004 int data;
#0005 } Node;
#0006
#0007 typedef struct _List
#0008 {
#0009 Node *head;
#0010 CRITICAL_SECTION critical_sec;
#0011 } List;
#0012
#0013 List *CreateList()
#0014 {
#0015 List *pList = (List *)malloc(sizeof(pist));
#0016 pList->head = NULL;
#0017 InitializeCriticalSection(&pList->critical_sec);
#0018 return pList;
#0019 }
#0020
#0021 void DeleteList(List *pList)
#0022 {
#0023 DeleteCriticalSection(&pList->critical_sec);
#0024 free(pList);
#0025 }
#0026
#0027 void AddHead(List *pList, Node *node)
#0028 {
#0029 EnterCriticalSection(&pList->critical_sec);
#0030 node->next = pList->head;
#0031 pList->head = node;
#0032 LeaveCriticalSection(&pList->critical_sec);
#0033 }
#0034
#0035 void Insert(List *pList, Node *afterNode, Node *newNode)
#0036 {
#0037 EnterCriticalSection(&pList->critical_sec);
#0038 if (afterNode == NULL)
{
#0039 AddHead(pList, newNode);
#0040 }
#0041 else
#0042 {
#0043 newNode->next = afterNode->next;
#0044 afterNode->next = newNode;
#0045 }
#0046 LeaveCriticalSection(&pList->critical_sec);
#0047 }
#0048
#0049 Node *Next(List *pList, Node *node)
#0050 {
#0051 Node* next;
#0052 EnterCriticalSection(&pList->critical_sec);
#0053 next = node->next;
#0054 LeaveCriticalSection(&pList->critical_sec);
#0055 return next;
#0056 }
加上了额外的 critical section 操作之后,同一时间里最多就只有一个人能够读(或写)链表内容。请注意,我把 CRITICAL_SECTION 变量放在 List 结构之中。你也可以使用一个全局变量取代之,但我是希望每一个链表实体都能够独立地读写。如果只使用一个全局性 critical section,就表示一次只能读写一个链表,这会产生效率上的严重问题。
你或许纳闷,为什么 Next() 也需要环绕一个 critical section,毕竟它只是处理单一一个值而已。还记得吗,第1章曾经说过,return node->next 实际上被编译为数个机器指令,而不是一个“不可分割的操作”(所谓的 atomicoperation)。如果我们在前后加上 critical section 的保护,就能够强迫该操作成为“不可分割的”。
上述程序代码存在着一个微妙点。在 Next() 离开 critical section 之后,但尚未 return 之前,没有什么东西能够保护这个 node 免受另一个线程的删除操作。这个问题可以靠更高阶的“readers/writers 锁定”解决之。我们将在第7章解释怎么做。
这个简短的例子也说明了 Win32 critical section 的另一个性质。一旦线程进入一个 critical section,它就能够一再地重复进入该 critical section。这也就是为什么 Insert() 可以调用 AddHead() 而不需先调用 LeaveCriticalSection()的缘故。唯一的警告就是,每一个“进入”操作都必须有一个对应的“离开”操作。如果某个线程调用 EnterCriticalSection() 5 次, 它也必须调用LeaveCriticalSection() 5 次,该 critical section 才能够被释放。
最小锁定时间
在任何关于同步机制的讨论中,不论是在 Win32 或 Unix 或其他操作系统,你一定会一再地听到这样一条规则:不要长时间锁住一份资源
如果你一直让资源被锁定,你就会阻止其他线程的执行,并把整个程序带到一个完全停止的状态。以 critical section 来说,当某个线程进入 criticalsection 时,该项资源即被锁定。
我们很难定义所谓“长时间”是多长。如果你在网络上进行操作,并且是在一个拨号网络上,长时间可能是指数分钟。如果你所处理的是应用程序的一项关键性资源,长时间可能是指数个毫秒(milliseconds)。
我能够给你的最牢靠而最立即的警告就是,千万不要在一个 critical section 之中调用 Sleep() 或任何 Wait...() API 函数。
当你以一个同步机制保护一份资源时,有一点必须常记在心,那就是:这项资源被使用的频率如何?线程必须多快释放这份资源,才能确保整个程序的运作很平顺?
某些人会关心这样的问题:如果我再也不释放资源(或不离开 critical section,或不释放 mutex……等等),会怎样?答案是:不会怎样!
操作系统不会当掉。用户不会获得任何错误信息。最坏的情况是,当主线程(一个 GUI 线程)需要使用这被锁定的资源时,程序会挂在那儿,动也不动。真的,同步机制并没有什么神奇魔法。
避免Dangling Critical Sections
Critical section 的一个缺点就是,没有办法获知进入 critical section 中的那个线程是生是死。从另一个角度看,由于 critical section 不是核心对象,如果进入 critical section 的那个线程结束了或当掉了, 而没有调用LeaveCriticalSection() 的话,系统没有办法将该 critical section 清除。如果你需要那样的机能,你应该使用 mutex(本章稍后将介绍 mutex)。
Jeffrey Richter 在他所主持的 Win32 Q&A 专栏(Microsoft Systems ournal,1996/07)中曾经提到过,Windows NT 和 Windows 95 在管理 dangling critical sections 时有极大的不同。在 Windows NT 之中,如果一个线 程进入某个 critical section 而在未离开的情况下就结束,该 critical section 会被永远锁住。然而在 Windows 95 中,如果发生同样的事情,其他等着要进入该 critical section 的线程,将获准进入。这基本上是一个严重的问题,因为你竟然可以在你的程序处于不稳定状态时进入该 critical section。
死锁(Deadlock)
第4章 同步控制 Synchronization ----critical section 互斥区 ,临界区的更多相关文章
- 第4章 同步控制 Synchronization ----互斥器(Mutexes)
Win32 的 Mutex 用途和 critical section 非常类似,但是它牺牲速度以增加弹性.或许你已经猜到了,mutex 是 MUTual EXclusion 的缩写.一个时间内只能够有 ...
- 第4章 同步控制 Synchronization ----同步机制的摘要
同步机制摘要Critical Section Critical section(临界区)用来实现"排他性占有".适用范围是单一进程的各线程之间.它是: 一个局部性对象,不是一个核 ...
- 第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 ...
- 第4章 同步控制 Synchronization ----事件(Event Objects)
Win32 中最具弹性的同步机制就属 events 对象了.Event 对象是一种核心对象,它的唯一目的就是成为激发状态或未激发状态.这两种状态全由程序来控制,不会成为 Wait...() 函数的副作 ...
- 第4章 同步控制 Synchronization ----信号量(Semaphore)
许多文件中都会提到 semaphores(信号量),因为在电脑科学中它是最具历史的同步机制.它可以让你陷入理论的泥淖之中,教授们则喜欢问你一些有关于信号量的疑难杂 症.你可能不容易找到一些关于 sem ...
- Jmeter-Critical Section Controller(临界区控制器)
The Critical Section Controller ensures that its children elements (samplers/controllers, etc.) will ...
- Jmeter-Critical Section Controller(临界区控制器)(还没看,是一个控制请求按顺序执行的东东)
The Critical Section Controller ensures that its children elements (samplers/controllers, etc.) will ...
随机推荐
- Bash脚本编写初体验
上周例会的时候,冷不丁的接到了维护原有的安装脚本和编写升级.卸载脚本的任务,PM和几个同事一本正经的说,一天甚至30分钟就可以精通shell脚本编写,哪怕没有语言基础也可以. 当然,作为有着C++.P ...
- spring整合mybatis错误:Caused by: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 62; 文档根元素 "mapper" 必须匹配 DOCTYPE 根 "configuration"。
运行环境:jdk1.7.0_17+tomcat 7 + spring:3.2.0 +mybatis:3.2.7+ eclipse 错误:Caused by: org.xml.sax.SAXParseE ...
- IOS学习【前言】
2016-1-14 16年开始时导师安排任务,开始IOS学习之旅 经过几天的学习,感觉还是需要作比较多的学习笔记,因此开始用博客记录整个过程,方便以后查看学习与分享. 主要记录一些关键的问题处理方法 ...
- YYHS-挑战nbc
题目描述 Abwad是一名有志向的优秀OI少年.遗憾的是,由于高能宇宙射线的影响,他不幸在NOI中滚粗.不过,Abwad才高一,还有许许多多的机会.在长时间的刻苦学习之后,他实力大增,并企图撼动OI界 ...
- Web in Linux小笔记001
Linux灾难恢复: Root密码修复 Centos single Filesystem是硬盘文件根目录,无法再cd ..就像macitosh 硬盘图标 Pwd:显示绝对路径 MBR修复 模拟MBR被 ...
- 201521123083《Java程序设计》第9周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. 2. 书面作业 本次PTA作业题集异常 1.常用异常 题目5-1 1.1 截图你的提交结果(出现学号) 1.2 自己 ...
- Java 第八周总结
1. 本周学习总结 2. 书面作业 1.List中指定元素的删除 1.1 实验总结 list中可以通过list.get(i)来获取具体第几个的元素的值,再通过compareTo来对比 通过in.has ...
- 201521123078 《Java程序设计》 第8周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 1.List中指定元素的删除(题目4-1) 1.1 实验总结 public static vo ...
- 201521123103 《Java学习笔记》 第六周学习总结
一.本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 二.书面作业 1.clone方法 1.1 Object对象中 ...
- 201521123007《Java程序设计》第5周学习总结
1. 本周学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 2. 书面作业 作业参考文件下载 1. 代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.jav ...