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

来源:互联网 发布:商务软件解决方案 编辑:程序博客网 时间:2024/06/05 16:58

2011 年 09 月 05 日 by name5566

Win32 提供了不少同步机制。

Critical Sections(临界区)

Critical sections 是 Win32 最容易使用的同步机制。Critical sections 指的是一块用于处理共享资源的代码,如果一个线程已经进入了某个 Critical section,其他线程就不能进入此 Critical section 了。

我们常常为每一个需要保护的共享资源都声明一个 CRITICAL_SECTION 变量。因为 Critical sections 不是内核对象,因此无所谓 HANDLE 这样的东西。

  1. // 初始化 CRITICAL_SECTION 变量
  2. // 在使用 CRITICAL_SECTION 前应该初始化此变量
  3. // 和 DeleteCriticalSection 成对出现
  4. void WINAPI InitializeCriticalSection(
  5. LPCRITICAL_SECTION lpCriticalSection
  6. );
  7.  
  8. // 清理 CRITICAL_SECTION 变量
  9. // 和 InitializeCriticalSection 成对出现
  10. void WINAPI DeleteCriticalSection(
  11. LPCRITICAL_SECTION lpCriticalSection
  12. );
  13.  
  14. // 锁定 CRITICAL_SECTION 变量
  15. // 和 LeaveCriticalSection 要成对出现
  16. void WINAPI EnterCriticalSection(
  17. LPCRITICAL_SECTION lpCriticalSection
  18. );
  19.  
  20. // 解锁 CRITICAL_SECTION 变量
  21. // 和 EnterCriticalSection 要成对出现
  22. void WINAPI LeaveCriticalSection(
  23. LPCRITICAL_SECTION lpCriticalSection
  24. );

我们来看一个 Critical sections 的常见用法,一个线程安全的 List:

  1. typedef struct _Node
  2. {
  3. struct _Node* next;
  4. int data;
  5. } Node;
  6.  
  7. typedef struct _List
  8. {
  9. Node* head;
  10. // 每一个 List 保存一个 CRITICAL_SECTION 变量
  11. CRITICAL_SECTION critical_sec;
  12. } List;
  13.  
  14. List* CreateList()
  15. {
  16. List* pList = (List*)malloc(sizeof(pList));
  17. pList->head = NULL;
  18. // 创建 List 的时候,初始化 CRITICAL_SECTION 变量
  19. InitializeCriticalSection(&pList->critical_sec);
  20. return pList;
  21. }
  22.  
  23. void DeleteList(List* pList)
  24. {
  25. // 删除 List 的时候,释放 CRITICAL_SECTION 变量
  26. DeleteCriticalSection(&pList->critical_sec);
  27. free(pList);
  28. }
  29.  
  30. void AddHead(List* pList, Node* node)
  31. {
  32. // 锁定 CRITICAL_SECTION 变量
  33. EnterCriticalSection(&pList->critical_sec);
  34. node->next = pList->head;
  35. pList->head = node;
  36. // 解锁 CRITICAL_SECTION 变量
  37. LeaveCriticalSection(&pList->critical_sec);
  38. }
  39.  
  40. void Insert(List* pList, Node* afterNode, Node* newNode)
  41. {
  42. EnterCriticalSection(&pList->critical_sec);
  43. if (afterNode == NULL)
  44. AddHead(pList, newNode);
  45. else
  46. {
  47. newNode->next = afterNode->next;
  48. afterNode->next = newNode;
  49. }
  50. LeaveCriticalSection(&pList->critical_sec);
  51. }
  52.  
  53. Node* Next(List* pList, Node* node)
  54. {
  55. Node* next;
  56. // 这里也加上了 EnterCriticalSection
  57. // 因为 return node->next 这不是原子操作
  58. EnterCriticalSection(&pList->critical_sec);
  59. next = node->next;
  60. LeaveCriticalSection(&pList->critical_sec);
  61. // 注意,虽然这里使用了 Critical section 还是会存在问题
  62. // 因为在 LeaveCriticalSection 和 return 语句之间
  63. // next 变量对应的 Node 可能已经被其他线程删除了
  64. // 这里需要用 "reader / writer 锁定" 来解决(本文不谈及此问题)
  65. return next;
  66. }

从上面的代码可以看出,有好几个函数调用 EnterCriticalSection 和 LeaveCriticalSection 都使用的是同一个 CRITICAL_SECTION 变量作为参数,我们务必要形成一个观念就是:一个 CRITICAL_SECTION 变量总是且只是和某个需要保护的共享资源关联。
在 API 的使用上,关键的一点是一个线程可以重复的进入某个 Critical section:

  1. EnterCriticalSection(&c);
  2. // 这里不需要调用 LeaveCriticalSection
  3. // 线程还可以继续进入此 Critical section
  4. EnterCriticalSection(&c);
  5.  
  6. // Do something ...
  7.  
  8. // 应该调用两次 LeaveCriticalSection
  9. LeaveCriticalSection(&c);
  10. LeaveCriticalSection(&c);

Critical section 不是内核对象,你必须手动调用 LeaveCriticalSection 来进行清理,系统无法帮你做任何的工作,一个进入 Critical section 的线程如果宕掉了或者结束掉了,而在这之前没有调用 LeaveCriticalSection 的话,系统是没有办法清理该 Critical section 的。

死锁(deadlock)和 Mutexes

死锁的一个例子:

  1. void SwapList(List* list1, List* list2)
  2. {
  3. Node* tmp;
  4. EnterCriticalSection(&list1->critical_sec);
  5. EnterCriticalSection(&list2->critical_sec);
  6. tmp = list1->head;
  7. list1->head = list2->head;
  8. list2->head = tmp;
  9. LeaveCriticalSection(&list1->critical_sec);
  10. LeaveCriticalSection(&list2->critical_sec);
  11. }

我们假定线程 A 调用 SwapList(a, b); 线程 B 调用 SwapList(b, a); 且同时发生,线程 A 在执行了第一个 EnterCriticalSection 后发生了 Context switch,List a 的 CRITCAL_SECTION 变量被锁定,线程 B 执行 SwapList,将 List b 的 CRITICAL_SECTION 变量锁定并且等待 List a 的 CRITICAL_SECTION 变量解锁,而线程 A 继续执行时将等待 List b 的 CRITICAL_SECTION 变量解锁。这样,线程 A 等待线程 B 对 List b 的 CRITICAL_SECTION 变量解锁,而线程 B 等待线程 A 对 List a 的 CRITICAL_SECTION 变量解锁,两个线程相互等待,形成死锁。
只要一段代码需要两个(或者更多)共享资源时,就存在出现死锁的风险。多线程开发中,我们必须意识到每行语句或者每两行语句间都可能发生 Context switch,务必要逐行的分析代码。
我们可以使用 all-or-nothing(要么获得所有资源,要么任何资源都不获取)来避免死锁的发生。

为了使用 all-or-nothing 来避免死锁的发生,我们需要使用这样的机制:线程能够等待两个(或者更多)资源被释放。WaitForMultipleObjects 符合我们的要求(能够等待多个内核对象进入 signaled 状态),但是其只能用于内核对象(而 Critical section 不是内核对象),这时候我们需要使用 mutexes 内核对象。

Mutexes 以牺牲运行效率作为代价用以增加弹性,其和 Critical section 的差别在于:

  • 效率上来说,锁定一个 Mutex 比锁定一个 Critical section 要花 100 倍的时间。因为 Critical section 只需要在用户模式(user mode)下就可以操作了
  • Mutexes 可以跨进程使用,Critical section 只能在同一个进程中使用
  • 等待一个 Mutex 的时候,可以指定一个超时时间,而 Critical section 则不可以指定

相关的 API:

  1. // 创建一个 Mutex
  2. // 成功返回一个 Handle,失败返回 NULL
  3. // 如果指定名称的 Mutex 已经存在,函数调用并不会失败
  4. // 但是可以调用 GetLastError(),这是其返回 ERROR_ALREADY_EXISTS
  5. HANDLE WINAPI CreateMutex(
  6. // security 属性,如果为 NULL 表示使用默认值
  7. LPSECURITY_ATTRIBUTES lpMutexAttributes,
  8. // 调用 CreateMutex 的线程是否拥有此 Mutex(拥有的含义见下面的论述)
  9. BOOL bInitialOwner,
  10. // Mutex 的名称(系统唯一)
  11. // 任何的进程或者线程都可以通过此名称使用此 Mutex
  12. // 此名称也可以为空(NULL)
  13. LPCTSTR lpName
  14. );

当没有任何一个线程拥有 Mutex,此 Mutex 就进入 signaled 状态,拥有一个 Mutex 的方法是调用 Wait… 函数(例如,WaitForMultipleObjects)。注意,一次只能有一个线程拥有某个 Mutex。

  1. void f()
  2. {
  3. // WaitForSingleObject 对 Mutexes 的作用和
  4. // EnterCriticalSection 对 Critical section 的作用类似
  5. //
  6. // 第一次调用时,Mutex 处于 signaled 状态(因为没有任何线程拥有它)
  7. // 函数返回 WAIT_OBJECT_0
  8. // 同时 Mutex 进入 nonsignaled 状态(因为 WaitForSingleObject 的调用使得线程拥有它)
  9. // 其他线程执行到此时,将等待此 Mutex 内核对象进入 signaled 状态
  10. // 注意,Mutexes 的拥有线程无论调用多少次 WaitForSingleObject 也不会被阻塞
  11. ::WaitForSingleObject(hMutex, INFINITE);
  12.  
  13. // ...
  14.  
  15. // 释放(解锁) Mutex
  16. // 使得 Mutex 进入 signaled 状态
  17. ::ReleaseMutex(hMutex);
  18. }
  19.  
  20. // 用于释放(解锁) Mutexes
  21. // 成功返回 TRUE,失败返回 FALSE
  22. BOOL ReleaseMutex(
  23. HANDLE hMutex
  24. );

同于其他的内核对象,调用 CloseHandle 来释放 Mutexes(使其引用计数减 1)。如果所有与某个 Mutex 相关的线程都结束了,那么此 Mutex 会被系统自动清除。在前面(http://name5566.com/745.html)谈到的,如果线程调用 Wait… 函数等待的是一个 Mutex 内核对象,那么如果拥有此 Mutex 的线程结束了(没有调用 ReleaseMutex 就结束了,可能是宕掉了或者是调用 ExitThread API),等待线程会得到 WAIT_ABANDONED 的通知(也就是 Wait… 函数的返回值)。

现在,我们可以用 Mutexes 来避免死锁了。我们在相同的时间把所有共享资源锁住(all-or-nothing):

  1. // 改造之后的 SwapList
  2. void SwapList(List* list1, List* list2)
  3. {
  4. Node* tmp;
  5.  
  6. HANDLE mutex[2];
  7. mutex[0] = list1->hMutex;
  8. mutex[1] = list2->hMutex;
  9.  
  10. // all-or-nothing
  11. // 一次把多个共享资源锁住
  12. ::WaitForMultipleObjects(2, mutex, TRUE, INFINITE);
  13. tmp = list1->head;
  14. list1->head = list2->head;
  15. list2->head = tmp;
  16. ReleaseMutex(mutex[0]);
  17. ReleaseMutex(mutex[1]);
  18. }

另外还有一个需要强调一下的是:

  1. // 代码片段 1
  2. HANDLE hMutex = ::CreateMutex(NULL, FALSE, "Sample Name");
  3. ::WaitForSingleObject(hMutex, INFINITE);
  4.  
  5. // 代码片段 2
  6. ::CreateMutex(NULL, TRUE, "Sample Name");

我们不应该写出代码片段 1 这样的代码,而应该使用代码片段 2,因为 Mutexes 可以被其他进程或者线程使用,因此,如果在 ::CreateMutex 调用完成之后 ::WaitForSingleObject 调用之前发生 Context Switch,这时候其他线程是可以使用此 Mutex 的,也就是说有可能出现此 Mutex 被其他线程先拥有的情况。使用代码片段 2 就不会有类似的问题。

信号量(Semaphores)

Semaphores 也是一个内核对象,相比 Mutexes 只能被锁定一次而言,Semaphores 允许被锁定 n 次(Semaphores 会被在锁定 n 次后才进入 nonsignaled 状态,而 Mutexes 在锁定一次后就进入 nonsignaled 状态。也就是说,signaled 状态为可以锁定状态,nonsignaled 状态为不可以锁定状态)。Semaphores 主要是用于解决各种 Producer / Consumer(生产者 / 消费者)问题的。看一个例子,现在有 n 辆相同的车,有多个人希望租车(一人租一辆),那么有几种做法:

  1. 为每个车加上 Mutexes 保护它们一次只能被一个人租用,这样,如果出租的车的数量很多的情况下,将需要创建很多的 Mutexes
  2. 使用一个 Mutexes 保护所有的待租售的车,这样效率很低,因为只能一次出租一辆车

一种较好的做法是,使用 Semaphores。首先我们先来了解一下 Semaphores 的 API:

  1. // 如果成功返回一个 Handle,失败返回 NULL
  2. // 如果指定的 Semaphore 的名称已经存在
  3. // GetLastError() 返回 ERROR_ALREADY_EXISTS
  4. HANDLE CreateSemaphore(
  5. // security 属性,NULL 表示使用默认值
  6. LPSECURITY_ATTRIBUTES lpAttributes,
  7. // Semaphores 的初始值,必须大于等于 0 并且小于或等于 lMaximumCount
  8. LONG lInitialCount,
  9. // Semaphores 的最大值
  10. LONG lMaximumCount,
  11. // Semaphores 的名字(系统唯一)
  12. // 任何的进程或者线程都可以通过此名称使用此 Semaphore
  13. // 此名称也可以为空(NULL)
  14. LPCTSTR lpName
  15. );

Semaphores(计数器)值为 0 时处于 nonsignaled 状态,值大于 0 时处于 signaled 状态。我们通过 Wait… 函数来锁定 Semaphores,每当锁定成功一次,Semaphores 的值就减少 1,一旦 Semaphores 的值为 0 则进入 nonsignaled 状态,Wait… 函数的调用将阻塞线程。解除锁定调用 ReleaseSemaphore:

  1. // 解除 Semaphores 的锁定
  2. // 此函数会将 Semaphores 的值加 lReleaseCount
  3. // 函数成功返回 TRUE,失败返回 FALSE
  4. BOOL WINAPI ReleaseSemaphore(
  5. // Semaphores 的 Handle
  6. HANDLE hSemaphore,
  7. // 为 Semaphores 增加的值
  8. LONG lReleaseCount,
  9. // 返回 Semaphores 原来的值
  10. LPLONG lpPreviousCount
  11. );

解决 Producer / Consumer 问题:

  1. // 必须使用两个 Semaphores
  2. // 一个用于让生产者进入等待(当仓库已满时)
  3. // 一个用于让消费者进入等待(当仓库已空时)
  4. HANDLE g_hProducerSemaphore;
  5. HANDLE g_hConsumerSemaphore;
  6.  
  7. // 用于保护(共享资源)仓库
  8. CRITICAL_SECTION g_bufferCS;
  9.  
  10. // 生产者线程函数
  11. DWORD WINAPI Producer(LPVOID p)
  12. {
  13. while (true)
  14. {
  15. // 生产一个 Widget
  16. Widget* widget = ProduceWidget();
  17.  
  18. // g_hProducerSemaphore 的值为 0 时
  19. // 表示仓库已满,生产者等待 Widget 被消费
  20. ::WaitForSingleObject(g_hProducerSemaphore, INFINITE);
  21.  
  22. // 将生产出来的 Widget 放入仓库
  23. ::EnterCriticalSection(&g_bufferCS);
  24. PushWidget(widget);
  25. ::LeaveCriticalSection(&g_bufferCS);
  26.  
  27. // g_hConsumerSemaphore 的值增加 1
  28. // 表示仓库中的 Widget 增加 1
  29. LONG dummy;
  30. ::ReleaseSemaphore(g_hConsumerSemaphore, 1, &dummy);
  31. }
  32.  
  33. return 0;
  34. }
  35.  
  36. // 消费者线程函数
  37. DWORD WINAPI Consumer(LPVOID p)
  38. {
  39. while (true)
  40. {
  41. // g_hConsumerSemaphore 的值为 0 时
  42. // 表示仓库已空,消费者等待 Widget 被生产
  43. ::WaitForSingleObject(g_hConsumerSemaphore, INFINITE);
  44.  
  45. // 从仓库中获取一个 Widget
  46. ::EnterCriticalSection(&g_bufferCS);
  47. Widget* widget = PopWidget();
  48. ::LeaveCriticalSection(&g_bufferCS);
  49.  
  50. // g_hProducerSemaphore 的值增加 1
  51. // 表示仓库中的 Widget 减少 1
  52. LONG dummy;
  53. ::ReleaseSemaphore(g_hProducerSemaphore, 1, &dummy);
  54.  
  55. // 消费一个 Widget
  56. ConsumeWidget(widget);
  57. }
  58.  
  59. return 0;
  60. }

事件(Event Objects)

Win32 中最具有弹性的同步机制就是 events 对象了。Events 对象是一种内核对象,存在 signaled 状态和 nonsignaled 状态,这两个状态的设置完全由程序员自己控制,而不会被 Wait… 函数改变。

  1. // 产生一个 event 对象
  2. // 成功该函数返回一个 Handle,失败返回 NULL
  3. // 如果指定名称的 Event 已经存在,函数调用并不会失败
  4. // 但是可以调用 GetLastError(),这是其返回 ERROR_ALREADY_EXISTS
  5. HANDLE WINAPI CreateEvent(
  6. // security 属性,NULL 表示使用默认属性
  7. LPSECURITY_ATTRIBUTES lpEventAttributes,
  8. // 是否需要手动重置
  9. // 如果为 FALSE 表示 events 在变成 signaled 状态之后
  10. // 自动重置为 nonsignaled 状态
  11. // 如果为 TRUE 表示不会进行自动重置(需调用 ResetEvent())
  12. BOOL bManualReset,
  13. // 如果为 TRUE 表示一开始此 event 就处于 signaled 状态
  14. // FALSE 则表示此 event 一开始处于 nonsignaled 状态
  15. BOOL bInitialState,
  16. // events 的名字(系统唯一)
  17. // 任何的进程或者线程都可以通过此名称使用此 event
  18. // 此名称也可以为空(NULL)
  19. LPCTSTR lpName
  20. );

Events 相关 API 说明:

  • BOOL SetEvent(HANDLE hEvent) — 将某个 event 设置为 signaled 状态
  • BOOL ResetEvent(HANDLE hEvent) — 将某个 event 设置为 nonsignaled 状态
  • BOOL PulseEvent(HANDLE hEvent) — 设置某个 event 对象为 signaled 状态,唤醒等待线程,恢复为 nonsignaled 状态

特别要注意的是:

  • 如果 Event 是一个自动重置的 Event,此 Event 进入 signaled 状态后会唤醒且只唤醒一个等待线程,然后又进入 nonsignaled 状态
  • 如果 Event 是一个手动重置的 Event,此 Event 进入 signaled 状态后会唤醒所有等待线程,其不会自动进入 nonsiganled 状态

Interlocked Variables

最简单的同步机制是使用 Interlocked 函数,此函数执行极快:

  1. // Addend 指向一个 32 位变量
  2. // 以下两个函数将 Addend 指向的变量递增或者递减
  3. // Addend 指向的变量被递增或者递减后
  4. // 如果其值等于 0 则函数返回值为 0
  5. // 如果其值大于 0 则函数返回值大于 0
  6. // 如果其值小于 0 则函数返回值小于 0
  7. //
  8. // 此函数保证对 Addend 指向的变量的操作是一个原子操作
  9. // 对于 64 位变量的操作使用 InterlockedIncrement64 和 InterlockedDecrement64
  10. LONG InterlockedIncrement(LONG volatile *Addend);
  11. LONG InterlockedDecrement(LONG volatile *Addend);
  12.  
  13. // 此函数用于为 Target 指向的变量设定一个新值并保证整个操作是一个原子操作
  14. // 函数返回值为 Target 之前指向的变量的值
  15. LONG InterlockedExchange(
  16. // 32 位变量的地址
  17. LONG volatile *Target,
  18. // 设定到 Target 指向的变量的值
  19. LONG Value
  20. );

注意,我们应该确保传给 Interlocked 函数的变量地址是对齐的,否则这些函数调用可能失败。

总结各个同步控制机制:

  • Critical sections
    1. 不是内核对象
    2. 只能用于用于单个进程
    3. 效率高(无需进入 Kernel mode)
    4. 一个线程不能同时等待一个以上的 Critical section(因此特定情况下存在死锁风险)
    5. 锁定 Critical section 的线程结束(或者宕掉)时系统无法自动清理 Critical section
  • Mutexes
    1. 是内核对象
    2. 可以用于多个进程
    3. 效率比 Critical section 低许多
    4. 一个线程可以同时等待一个以上的 Mutexes(通过 WaitForMultipleObjects)
    5. 锁定(拥有) Mutexes 的线程结束(或者宕掉)时系统会通知其他等待此 Mutex 的线程(Wait… 返回 WAIT_ABANDONED_?),仅有 Mutexes 具有此特性
    6. 存在拥有权
  • Semaphores
    1. 是内核对象
    2. 可以用于多个进程
    3. 主要被用于解决 Producer / Consumer(生产者 / 消费者)问题
    4. 不存在拥有权
  • Events
    1. 是内核对象
    2. 可以用于多进程
    3. 比较灵活,完全在程序的掌控之下
    4. 主要被用于 Overlapped IO 或者设计自定义的同步对象
  • Interlocked Variables
    和其他的同步机制用途差别很大,其效率是最高的
原创粉丝点击