本章讨论 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 互斥区 ,临界区的更多相关文章

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

    Win32 的 Mutex 用途和 critical section 非常类似,但是它牺牲速度以增加弹性.或许你已经猜到了,mutex 是 MUTual EXclusion 的缩写.一个时间内只能够有 ...

  2. 第4章 同步控制 Synchronization ----同步机制的摘要

    同步机制摘要Critical Section Critical section(临界区)用来实现"排他性占有".适用范围是单一进程的各线程之间.它是:  一个局部性对象,不是一个核 ...

  3. 第4章 同步控制 Synchronization ---哲学家进餐问题(The Dining Philosophers)

    哲学家进餐问题是这样子的:好几位哲学家围绕着餐桌坐,每一位哲学家要么思考,要么等待,要么就吃饭.为了吃饭,哲学家必须拿起两支筷子(分放于左右两端).不幸的是,筷子的数量和哲学家相等,所以每支筷子必须由 ...

  4. 第4章 同步控制 Synchronization ----Interlocked Variables

    同步机制的最简单类型是使用 interlocked 函数,对着标准的 32 位变量进行操作.这些函数并没有提供"等待"机能,它们只是保证对某个特定变量的存取操作是"一个一 ...

  5. 第4章 同步控制 Synchronization ----死锁(DeadLock)

    Jeffrey Richter 在他所主持的 Win32 Q&A 专栏(Microsoft Systems Journal,1996/07)中曾经提到过,Windows NT 和 Window ...

  6. 第4章 同步控制 Synchronization ----事件(Event Objects)

    Win32 中最具弹性的同步机制就属 events 对象了.Event 对象是一种核心对象,它的唯一目的就是成为激发状态或未激发状态.这两种状态全由程序来控制,不会成为 Wait...() 函数的副作 ...

  7. 第4章 同步控制 Synchronization ----信号量(Semaphore)

    许多文件中都会提到 semaphores(信号量),因为在电脑科学中它是最具历史的同步机制.它可以让你陷入理论的泥淖之中,教授们则喜欢问你一些有关于信号量的疑难杂 症.你可能不容易找到一些关于 sem ...

  8. Jmeter-Critical Section Controller(临界区控制器)

    The Critical Section Controller ensures that its children elements (samplers/controllers, etc.) will ...

  9. Jmeter-Critical Section Controller(临界区控制器)(还没看,是一个控制请求按顺序执行的东东)

    The Critical Section Controller ensures that its children elements (samplers/controllers, etc.) will ...

随机推荐

  1. Spring中AOP简介与切面编程的使用

    Spring中AOP简介与使用 什么是AOP? Aspect Oriented Programming(AOP),多译作 "面向切面编程",也就是说,对一段程序,从侧面插入,进行操 ...

  2. anaconda安装加速镜像问题解决

    Anaconda使用conda连接网络出现错误 我使用的是windows10 64bit下的Anaconda2,在安装和更新包的时候出现以下报错信息. 这是使用默认源安装包的报错信息: C:Users ...

  3. POI处理Excel中的日期数据类型

    在POI处理Excel中的日期类型的单元格时,如果仅仅是判断它是否为日期类型的话,最终会以NUMERIC类型来处理. 正确的处理方法是先判断单元格 的类型是否则NUMERIC类型, 然后再判断单元格是 ...

  4. 小程序脚本语言WXS,你想要的都在这里了

    WXS脚本语言是 Weixin Script脚本的简称,是JS.JSON.WXML.WXSS之后又一大小程序内部文件类型.截至到目前小程序已经提供了5种文件类型. 解构小程序的几种方式,其中一种方式就 ...

  5. RadioButtonList控件如何取得选中的值

    1.需求:我现在页面上有放两个单选控件,现在要通过判断不同的单选控件来对页面上的标签进行显示和隐藏操作 2.控件如下 <asp:RadioButtonList ID=" RepeatD ...

  6. base(function strchr)

    函数原型:extern char *strchr(char *str,char character) 参数说明:str为一个字符串的指针,character为一个待查找字符.        所在库名: ...

  7. 《Java程序设计》终极不改版

     半年前的作品,上传只为纪念~ 成绩: ____0.1______ Java程序设计  课程设计 题 目:大学生信息管理系统 学 院:  计算机与软件学院 专 业:     网络工程_____­ .  ...

  8. 转:【Java并发编程】之七:使用synchronized获取互斥锁的几点说明

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17199201     在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访 ...

  9. 团队作业4——第一次项目冲刺(ALpha版本)第四天

    一.Daily Scrum Meeting照片 二.燃尽图 三.项目进展 1.界面 完善了昨天的的代码---前端的HTML页面设计 2.功能 完成后台数据处理的全部基本功能: a.数据结构设计及数据交 ...

  10. 201521123118《java程序与设计》第8周学习总结

    1. 本周学习总结 1. 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 1. List中指定元素的删除(题目4-1) 1.1 实验总结 Scanner sc = new ...