第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() 就是异步行为。如下图:

第4章   同步控制 Synchronization    ----critical section  互斥区 ,临界区

在 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)

上一篇:大数据量下,分页的解决办法,bubuko.com分享,快乐人生


下一篇:【转】阿里出品的ETL工具dataX初体验