Win32 多线程程序设计(四)同步控制

多线程程序的一个最具挑战性的问题就是:如何让一个线程和另一个线程合作。

WaitForSingleObject  
WaitForSingleObjectEx  
WaitForMultipleObjects  
WaitForMultipleObjectsEx  
MsgWaitForMultipleObjects  
MsgWaitForMultipleObjectsEx  

InitializeCriticalSection  
InitializeCriticalSectionAndSpinCount  
TryEnterCriticalSection   
EnterCriticalSection  
LeaveCriticalSection  
DeleteCriticalSection  

CreateMutex  
OpenMutex  
ReleaseMutex  

CreateSemaphore  
OpenSemaphore  
ReleaseSemaphore  

CreateEvent  
OpenEvent  
ResetEvent   
SetEvent  
PulseEvent  

InterlockedCompareExchange  
InterlockedDecrement  
InterlockedExchange  
InterlockedExchangeAdd  
InterlockedIncrement  

4.1 Critical Sections(关键区域、临界区域)

最容易使用的一个同步机制,临界区广义地指一块内存、一个数据结构、一个文件,或任何其他具有“使用之排他性”的东西。

CRITICAL_SECTION 类型的变量。这个变量扮演红绿灯的角色,让同一时间内只有一个线程进入 critical section。

Critical section 并不是核心对象。因此,没有所谓 handle 这样的东西。它和核心对象不同,它存在于进程的内存空间中。

4.1.1 产生:InitializeCriticalSection()

VOID InitializeCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection
);

参数:
lpCriticalSection一个指针,指向欲被初始化的CRITICAL_SECTION变量。这个变量应该在你的程序中定义。

返回值:
此函数传回 void 。

4.1.2 清除:DeleteCriticalSection()

VOID DeleteCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection
);

参数:
lpCriticalSection  指向一个不再需要的 CRITICAL_SECTION 变量。

返回值
此函数传回void。

4.1.3 进入:EnterCriticalSection()

VOID EnterCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection
);

参数:
lpCriticalSection  指向一个你即将锁定的 CRITICAL_SECTION 变量。

返回值:
此函数传回 void 。

4.1.4 离开LeaveCriticalSection()

VOID LeaveCriticalSection(
    LPCRITICAL_SECTION lpCriticalSection
);

参数:
lpCriticalSection  指向一个你即将解除锁定的 CRITICAL_SECTION 变量。

返回值:
此函数传回 void 。
typedef struct _Node
{
    struct _Node *next;
    int data;
} Node;

typedef struct _List
{
    Node *head;
    CRITICAL_SECTION critical_sec;
} List;

List *CreateList()
{
    List *pList = (List *)malloc(sizeof(pist));
    pList->head = NULL;
    InitializeCriticalSection(&pList->critical_sec);
    return pList;
}

void DeleteList(List *pList)
{
    DeleteCriticalSection(&pList->critical_sec);
    free(pList);
}

void AddHead(List *pList, Node *node)
{
    EnterCriticalSection(&pList->critical_sec);
    node->next = pList->head;
    pList->head = node;
    LeaveCriticalSection(&pList->critical_sec);
}

void Insert(List *pList, Node *afterNode, Node *newNode)
{
    EnterCriticalSection(&pList->critical_sec);
    if (afterNode == NULL) 
    {
        AddHead(pList, newNode);
    }
    else
    {
        newNode->next = afterNode->next;
        afterNode->next = newNode;
    }
    LeaveCriticalSection(&pList->critical_sec);
}

Node *Next(List *pList, Node *node)
{
    Node* next;
    EnterCriticalSection(&pList->critical_sec);
    next = node->next;
    LeaveCriticalSection(&pList->critical_sec);
    return next;
}

Critical section 的一个缺点就是,没有办法获知进入 critical section 中的那个线程是生是死。从另一个角度看,由于 critical section 不是核心对象,如果 进 入 critical section 的那个线程结束了或当掉了,而 没 有 调 用LeaveCriticalSection() 的话,系统没有办法将该 critical section 清除。如果你需要那样的机能,你应该使用 mutex。

4.2 互斥器Mutexes

4.2.1 哲学家进餐

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

如果你允许死锁发生,哲学家会一次取得一支筷子,而当他手上
只有一支筷子时,他就是处于等待状态。如果你不允许死锁发生,哲学家要么一次获得两支筷子,要么就什么都得不到。

4.2.2 互斥器

mutex和critical section做相同的事情,但是它们还是有差别的:

  • 锁住一个未被拥有的 mutex,比锁住一个未被拥有的 critical section,需要花费几乎 100 倍的时间。因为 critical section 不需要进入操作系统核心,直接在“user mode”就可以进行操作。
  • Mutexes 可以跨进程使用。Critical section 则只能够在同一个进程中使用。
  • 等待一个 mutex 时,你可以指定“结束等待”的时间长度。但对于critical section 则不行。

全局性:mutex对系统而言是全局性的,其他程序也可以使用,通过mutex产生时指定的名称访问,一定要使用名称,因为你没有办法把handle 交给一个执行中的进程。

4.2.3 产生:CreateMutex

如果你调用 CreateMutex() 并指定一个早已存在的 mu tex 名称,Win32会回给你一个 mutex handle,而不会为你产生一个新的 mutex。

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner,
    LPCTSTR lpName
);

参数:
lpMutexAttributes:安全属性。NULL 表示使用默认的属性。
bInitialOwner:如果你希望“调用 CreateMutex() 的这个线程”拥有产生出来的mutex,就将此值设为TRUE。
lpName:mutex 的名称(一个字符串)。任何进程或线程都可以根据此名称使用这一mutex。名称可以是任意字符串,只要不含反斜线(backslash,\)即可。

返回值:
如果成功,则传回一个 handle,否则传回 NULL。调用 GetLastError() 可以获得更进一步的信息。如果指定的 mutex名称已经存在,GetLastError() 会传回 ERROR_ ALREADY_EXISTS

4.2.4 关闭:CloseHandle()

和其他核心对象一样,mutex 有一个引用计数。每次你调用CloseHandle(),引用计数便减 1。当引用计数达到 0 时,mutex 便自动被系统清除掉。

4.2.5 打开:OpenMutex()

如果 mutex 已经被产生了,并有一个名称,那么任何其他的进程和线程便可以根据该名称打开那个 mutex。

4.2.6 锁住:Wait…()

WaitForSingleObject()/WaitForMultipleObjects()

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

当没有任何线程拥有该 mutex ,而且有一个线程正以 Wait…() 等待该 mutex,该mutex 就会短暂地出现激发状态,使 Wait…() 得以返回。

4.2.7 释放:ReleaseMutex()

BOOL ReleaseMutex(
    HANDLE hMutex
);

参数:
hMutex  欲释放之 mu tex 的 handle 。

返回值:
如果成功,传回 TRUE 。如果失败,传回 FALSE 。

Mutex解决类似临界区无法结局的链表交换问题

void SwapLists(List *list, List *list2)
{
    List *tmp_list;
    EnterCriticalSection(list1->critical_sec);
    EnterCriticalSection(list2->critical_sec);
    tmp->list = list1->head;
    list1->head = list2->head;
    list2->head = temp->list;
    LeaveCriticalSection(list1->critical_sec);
    LeaveCriticalSection(list2->critical_sec);
}

当两个线程同时做表交换时,会产生互相等待死锁问题。用Mutex可以解决,Mutex可以一次把两个链表都占据,再处理,就不会出现抢占资源的情况。

struct Node
{
    struct Node *next;
    int data;
};

struct List
{
    struct Node *head;
    HANDLE hMutex;
};

struct List *CreateList()
{
    List *list = (List *)malloc(sizeof(struct List));
    list->head = NULL;
    list->hMutex = CreateMutex(NULL, FALSE, NULL);
    return list;
}

void DeleteList(struct List *list)
{
    CloseHandle(list->hMutex);
    free(list);
}

void SwapLists(struct List *list, struct List *list2)
{
    struct List *tmp_list;
    HANDLE arrhandles[2];

    arrhandles[0] = list1->hMutex;
    arrhandles[1] = list2->hMutex;
    WaitForMultipleObjects(2, arrHandles, TRUE, INFINITE);
    tmp_list = list1->head;
    list1->head = list2->head;
    list2->head = tmp_list;
    ReleaseMutex(arrhandles[0]);
    ReleaseMutex(arrhandles[1]);
}

4.3 信号量(Semaphores)

Win32 中的一个 semaphore 可以被锁住最多 n 次,其中 n 是 semaphore被产生时指定的。n 常常被设计用来代表“可以锁住一份资源”的线程个数。

论可以证明,mutex 是 semaphore 的一种退化。如果你产生一个semaphore 并令最大值为 1,那就是一个 mutex。也因此,mutex 又常被称为binary semaphore。

4.3.1 产生:CreateSemaphore()

HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCTSTR lpName
);
参数:

lpAttributes:安全属性。如果是 NULL 就表示要使用默认属性。
lInitialCount:semaphore 的初值。必须大于或等于 0,并且小于或等于lMaxim umCount。
lMaximumCount:Semaphore 的最大值。这也就是在同一时间内能够
锁住semaphore之线程的最多个数。
lpName:Semaphore 的名称(一个字符串)。任何线(或进程都可以根据这一名称引用到这个semaphore。这个值可以是NULL,意思是产生一个没有名字的semaphore。

返回值:
如果成功就传回一个handle ,否则传回 NULL 。不论哪一种情况,GetLastError() 都会传回一个合理的结果。如果指定的 semaphore 名称已经存在,则该函数还是成功的,GetLastError() 会传回 ERROR_ALREADY_EXISTS。

如果锁定成功,你也不会收到 semaphore 的拥有权。因为可以有一个以上的线程同时锁定一个 semaphore,所以谈 semaphore 的拥有权并没有太多帮助。在 semaphore 身上并没有所谓“独占锁定”这种事情。也因为没有拥有权的观念,一个线程可以反复调用 Wait…() 函数以产生新的锁定。这和 mutex绝不相同:拥有 mutex 的线程不论再调用多少次 Wait…() 函数,也不会被阻塞住。一旦 semaphore 的现值降到 0,就表示资源已经耗尽。此时,任何线程
如果调用 Wait…() 函数,必然要等待,直到某个锁定被解除为止。

4.3.2 解除锁定:ReleaseSemaphore()

当 你 调 用WaitForSingleObject() 并获得一个 semaphore 锁定之后,你就需要调用ReleaseSemaphore()。

BOOL ReleaseSemaphore(
    HANDLE hSemaphore,
    LONG lReleaseCount,
    LPLONG lpPreviousCount
);

参数:
hSemaphore:Semaphore 的 handle 。
lReleaseCount:Semaphore 现值的增额。该值不可以是负值或 0。
lpPreviousCount:藉此传回 semaphore 原来的现值。

返回值:
如果成功,则传回 TRUE。否则传回 FALSE。失败时可调用 GetLastError()
获得原因。

4.4 事件(Event Objects)

Event 对象是一种核心对象,它的唯一目的就是成为激发状态或未激发状态。这两种状态全由程序来控制,不会成为 Wait…() 函数的副作用。所以,你可以精确告诉一个event 对象做什么事,以及什么时候去做。

4.4.1 创建:CreateEvent()

HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,
    BOOL bManualReset,
    BOOL bInitialState,
    LPCTSTR lpName
);

参数:
lpEventAttributes:安全属性。NULL 表示使用默认属性。
bManualReset:如为 FALSE,表示这个 event 将在变成激发状态(因而唤醒一个线程)之后,自动重置(reset)为非激发状态。如果是 TRUE,表示不会自动重置,必须靠程序操作(调用 ResetEvent() )才能将激发状态的 event 重置为非激发状态。
bInitialState:如为 TRUE ,表示这个 event 一开始处于激发状态。如为 FALSE ,则表示这个 event 一开始处于非激发状态。
lpName:Event 对象的名称。任何线程或进程都可以根据这个文字名称,使用这一event 对象。

返回值:
如果调用成功,会传回一个 event handle,GetLastError() 会传回 0。如果lpName 所指定的 event 对象已经存在,CreateEvent() 传回的是该 eventhandle , 而不会产生一个新的。这时候 GetLastError() 会传 回ERROR_ALREADY_EXISTS。如果 CreateEvent() 失败,传回的是 NULL ,
GetLastError() 可以获得更进一步的失败信息。

4.4.2 非激活:ResetEvent()

BOOL ResetEvent(HANDLE hEvent);// 这个函数把指定的事件对象设置为未(非)受信状态。

4.4.3 激活:SetEvent()

BOOL SetEvent(HANDLE hEvent);// 这个函数把指定的事件对象设置为受信状态。

4.5 Interlocked Variables

保证对某个特定变量的存取操作是“一个一个接顺序来”。

LONG InterlockedIncrement(LPLONG lpTarget);
LONG InterlockedDecrement(LPLONG lpTarget);

参数:
lpTarget:32位变量的地址。这个变量内容将被递增或递减,结果将与 0 作比较。这个地址必须指向 long word。

返回值:
变量值经过运算(加 1 或减 1)后,如果等于 0,传回 0;如果大于 0,传回一个正值;如果小于 0,传回一个负值。

InterlockedExchange() 可 以 设 定 一 个 新 值 并 传 回 旧 值 。 就 像Increment/Decrement 函数一样,它提供了一个在多线程环境下的安全做法,用以完成一个很基础的运算操作。

LONG InterlockedExchange(
    LPLONG lpTarget,
    LONG lValue
);

参数:
lpTarget:32位变量的地址。这个指针必须指向 long word。
lValue:用以取代 lpTarget 所指内容之新值。

返回值:
传回先前由lpTarget 所指之内容。

发表回复