当前位置: 首页 > news >正文

【Win32 多线程程序设计基础第四章笔记】

🌹 作者: 云小逸
🤟 个人主页: 云小逸的主页
🤟 motto: 要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在!学会自己和解,与过去和解,努力爱自己。希望春天来之前,我们一起面朝大海,春暖花开!

🥇 专栏:

  • WTL学习
  • 动态规划
  • C 语言
  • C++
  • Java 语言
  • Linux 编程
  • 算法
  • 待续…

文章目录

    • 📚 前言
    • 一、同步控制的核心目标
      • 1. 典型问题示例:链表插入冲突
      • 2. 同步控制的作用
    • 二、同步(Synchronous)与异步(Asynchronous)的区别
      • 简单总结
    • 三、同步机制的作用类比
      • 1. 类比1:办公室管理者
      • 2. 类比2:红绿灯系统
    • 四、同步机制的使用原则
      • 1. 按需选择类型
      • 2. 支持组合使用
    • 五、Critical Sections(关键区域/临界区域)
      • 1. 定义与核心作用
        • (1)本质
        • (2)核心目标
      • 2. 关键特性
        • (1)非核心对象,无Handle
        • (2)基于“红绿灯”式排他控制
      • 3. 核心API与使用流程
        • (1)核心API详解
        • (2)标准使用流程
      • 4. 实际应用示例:用临界区保护链表操作
        • (1)链表结构设计
        • (2)关键操作的同步保护
        • (3)示例的核心优势
      • 5. 重要特性:可重入性
        • 示例代码
        • 规则
      • 6. 使用注意事项
        • (1)最小化锁定时间:避免长时间占用资源
        • (2)避免“Dangling Critical Sections”(悬垂临界区)
        • (3)明确共享资源范围
      • 7. 与“线程当老板”方案的对比优势
    • 六、死锁(Deadlock)
      • 1. 定义与本质
      • 2. 典型案例:SwapLists函数交换链表
        • (1)案例背景
        • (2)函数代码(原文示例)
        • (3)死锁产生的关键步骤
        • (4)最终结果
      • 3. 核心产生条件(基于案例提炼)
      • 4. 危害与处理原则
      • 5. 基础预防思路(文档提及)
    • 七、哲学家进餐问题(The Dining Philosophers)
      • 1. 问题的核心设定
        • (1)角色与资源
        • (2)哲学家的状态
      • 2. 死锁的产生过程(文档核心逻辑)
      • 3. DINING程序:可视化演示死锁行为
        • (1)程序的核心选项
        • (2)状态图例(文档明确说明)
        • (3)死锁的可视化表现
      • 4. 临界区的局限:为何无法解决该问题?
        • (1)临界区的初步尝试与缺点
        • (2)临界区的核心局限:非核心对象,无法批量等待
      • 5. 解决方向:引入Mutex(互斥器)的必要性
    • 八、互斥器(Mutexes)
      • 1. Mutex与Critical Section的核心区别
        • 补充:函数对应关系表(文档原文整理)
      • 2. Mutex的核心API与使用流程
        • (1)产生Mutex:`CreateMutex`
          • 关键说明:
        • (2)打开已存在的Mutex:`OpenMutex`或`CreateMutex`
          • 适用场景:
        • (3)锁住Mutex(获取拥有权):`Wait`系列函数
          • ① Mutex的激发状态规则(文档核心)
          • ② 等待流程示例(文档描述)
          • ③ 关键API参数
        • (4)释放Mutex(释放拥有权):`ReleaseMutex`
          • 关键注意点:
        • (5)销毁Mutex:`CloseHandle`
          • 注意:
      • 3. Mutex的特殊特性:处理“被舍弃的Mutex”
        • (1)被舍弃的场景
        • (2)应对建议
      • 4. Mutex的实际应用:两个经典案例
        • (1)案例1:哲学家进餐问题(解决死锁)
          • 场景回顾:
        • (2)案例2:修正SwapLists函数(解决循环等待)
          • 问题回顾:
      • 5. Mutex的适用场景总结
    • 九、信号量(Semaphores)
      • 1. 核心定位与作用
        • (1)本质:有限资源的“计数器”
        • (2)生活化实例:租车店的敞篷车(文档核心例子)
      • 2. 信号量与Mutex的核心区别(文档重点对比)
      • 3. 核心API与使用流程
        • (1)创建信号量:`CreateSemaphore`
          • 关键参数解析:
          • 返回值与错误处理:
        • (2)获取锁定:通过`Wait`系列函数
          • 核心逻辑:
          • 关键特性:
        • (3)释放锁定:`ReleaseSemaphore`
          • 关键规则:
          • 示例(基于租车场景):
      • 4. 信号量初值(`lInitialCount`)的意义
      • 5. 典型应用:生产者-消费者问题
      • 6. 适用场景总结
    • 十、事件(Event Objects)与互锁变量(Interlocked Variables)
      • 一、事件(Event Objects):完全可控的核心同步对象
        • 1. 核心定位:状态可控的“信号开关”
        • 2. 创建Event:`CreateEvent`函数与关键参数
          • 核心参数解析(决定Event行为的关键):
          • 返回值与错误处理:
        • 3. Event的核心操作函数
        • 4. EVENTTST程序:可视化演示Event行为
          • (1)AutoReset Event(`bManualReset=FALSE`)
          • (2)ManualReset Event(`bManualReset=TRUE`)
          • 程序额外启示:OS的线程公平性
        • 5. Event的关键局限性:信号遗失
          • 示例(文档描述的死锁场景):
      • 二、互锁变量(Interlocked Variables):轻量的32位变量原子操作
        • 1. 核心定位:解决“简单变量的race condition”
        • 2. 核心函数:3个原子操作函数
          • (1)`InterlockedIncrement`:原子加1
          • (2)`InterlockedDecrement`:原子减1
          • (3)`InterlockedExchange`:原子赋值并返回旧值
        • 3. 适用场景与优势
          • (1)核心适用场景
          • (2)与其他同步机制的区别
        • 4. 关键注意事项
      • 三、总结:两种机制的选型建议
    • 📣 结语

📚 前言

本章是Win32多线程程序设计的核心章节——同步控制。对于零基础的你来说,多线程就像多个工人同时装修一间房子,若没有统一的规则,工人可能互相干扰(比如同时抢用一把锤子、同时修改一面墙的设计),最终导致装修混乱。而“同步控制”就是给工人制定的规则,目的是让多线程在共享资源(如内存、数据结构、文件)时有序执行,避免“互相干扰”的问题。接下来,我们会按照“为什么需要同步→同步与异步的区别→同步机制的类比→具体同步工具(临界区、死锁、互斥器等)”的顺序,一步步带你理解Win32多线程的同步技术,所有例子和比喻都会保留,确保你能轻松跟上。

一、同步控制的核心目标

多线程程序的最大挑战,是让多个线程“同心协力”地访问共享资源。当多个线程同时操作同一资源(比如一块内存、一个链表、一个文件)时,若没有协调,会出现“多个线程同时修改同一数据”的情况,最终导致数据错误或程序异常。

1. 典型问题示例:链表插入冲突

正如第2章提到的“链表插入”问题:线程A正在修改链表节点时,被操作系统强制切换到线程B;线程B成功插入新节点后,线程A恢复执行,会覆盖线程B的操作,最终导致链表断裂(数据结构损坏)。

2. 同步控制的作用

同步控制的核心就是通过“规则”阻止这种冲突,让线程按顺序访问共享资源。简单说,就是给共享资源加“保护锁”,确保同一时间只有一个线程能操作它,就像让工人“排队使用工具”,避免争抢。

二、同步(Synchronous)与异步(Asynchronous)的区别

文档通过Win32 API的两个函数,清晰区分了这两个概念,就像生活中两种不同的办事方式:

类型核心逻辑Win32 API示例特点
同步调用方需等待被调用方完成,才能继续执行,类似“排队办事”——你必须等前面的人办完,才能轮到你。SendMessage()1. 调用后阻塞,直到窗口函数执行完毕;
2. 确保操作“有结果后再推进”。
异步调用方发送请求后直接继续执行,无需等待被调用方完成,类似“发消息留便条”——你把消息留下,不用等对方回复,先去做其他事。PostMessage()1. 仅将消息放入对方消息队列,立即返回;
2. 无法立即获取操作结果。

简单总结

  • 同步是“我等你做完”,强调“顺序性”;
  • 异步是“我先做我的”,强调“并行性”。

三、同步机制的作用类比

为了让你更好理解同步机制的角色,文档用了两个生活化例子:

1. 类比1:办公室管理者

若让一个线程当“管理者”,所有线程都要排队等它的指令才能操作资源。这种方式会导致效率极低——队伍过长时,大部分线程都在等待,完全违背多线程“高效计算”的需求,就像一个公司只有一个管理者审批所有事务,员工全在排队,工作进度严重滞后。

2. 类比2:红绿灯系统

同步机制更像红绿灯,为线程分配“通行权”(绿灯)或“等待权”(红灯)。它的核心原则是:确保每个线程都有机会获得绿灯,既避免多个线程同时抢资源(闯红灯),又不剥夺任何线程的执行机会(不会一直红灯),兼顾了秩序和效率。

四、同步机制的使用原则

Win32提供了多种同步机制,没有“万能方案”,需遵循以下原则使用:

1. 按需选择类型

Win32提供临界区、互斥器、信号量等多种同步机制,需根据问题场景选择。比如“同一进程内简单的资源保护”用临界区,“跨进程同步”用互斥器(后续章节会详细讲解每种机制的适用场景)。

2. 支持组合使用

基础同步机制可像“建筑积木”一样组合,设计更复杂的协调逻辑。例如用“信号量+事件对象”,能实现“多线程按批次处理任务”的需求——信号量控制每批线程数量,事件对象通知批次开始。

五、Critical Sections(关键区域/临界区域)

在Win32多线程同步机制中,Critical Sections(简称“临界区”) 是最基础、易用的同步工具,核心作用是确保“共享资源同一时间仅被一个线程访问”,避免竞争条件和数据破坏。

1. 定义与核心作用

(1)本质

临界区并非“区域”本身,而是保护共享资源的代码块的同步规则——它限定“处理共享资源的代码段”同一时间只能有一个线程进入执行。
文档明确:“所谓critical sections意指一小块‘用来处理一份被共享之资源’的程序代码”,这里的“共享资源”是广义的,包括一块内存、一个链表、一个文件等具有“使用排他性”的资源(同一时间只能被一个线程操作)。

(2)核心目标

解决多线程对共享资源的冲突访问问题。比如第1章中“链表插入被破坏”的场景:若两个线程同时执行AddHead插入节点,可能因线程切换导致链表断裂;而用临界区包裹插入代码后,可强制线程“排队”执行,避免冲突。

2. 关键特性

(1)非核心对象,无Handle

临界区不属于Win32核心对象(如线程、互斥器),因此没有“句柄(Handle)”,仅存在于当前进程的内存空间中。这意味着:

  • 无需通过Create类API创建,只需在程序中定义CRITICAL_SECTION类型变量并初始化;
  • 仅能用于同一进程内的线程同步,无法跨进程使用(跨进程需用后续的Mutex)。
(2)基于“红绿灯”式排他控制

每个共享资源对应一个CRITICAL_SECTION变量,该变量如同“红绿灯”:

  • 当一个线程调用EnterCriticalSection进入临界区时,“红灯亮起”,其他线程尝试进入时会被阻塞;
  • 当线程调用LeaveCriticalSection离开临界区时,“绿灯亮起”,阻塞的线程中会有一个被允许进入。

3. 核心API与使用流程

文档明确了临界区的4个核心操作API,以及标准使用流程:

(1)核心API详解
API函数功能参数说明返回值
InitializeCriticalSection初始化临界区变量lpCriticalSection:指向待初始化的CRITICAL_SECTION变量void(无返回值)
DeleteCriticalSection销毁临界区变量(释放相关资源)lpCriticalSection:指向待销毁的CRITICAL_SECTION变量void
EnterCriticalSection进入临界区(获取资源访问权)lpCriticalSection:指向目标临界区变量void
LeaveCriticalSection离开临界区(释放资源访问权)lpCriticalSection:指向目标临界区变量void
(2)标准使用流程

以文档中的全局临界区为例,流程如下(可直接复制使用):

// 1. 定义全局/共享的CRITICAL_SECTION变量(供所有线程访问)
CRITICAL_SECTION gCriticalSection;
void UseCriticalSection() {// 2. 初始化临界区(通常在程序启动时执行)InitializeCriticalSection(&gCriticalSection);// 3. 访问共享资源:进入临界区EnterCriticalSection(&gCriticalSection);/* 处理共享资源的代码(如修改全局变量、操作链表等) */// 4. 结束访问:离开临界区LeaveCriticalSection(&gCriticalSection);// 5. 程序退出前,销毁临界区DeleteCriticalSection(&gCriticalSection);
}

注意DeleteCriticalSection仅用于“销毁临界区变量的内部状态”,并非像C++delete那样释放内存,需与变量的内存管理(如malloc/free)区分开。

4. 实际应用示例:用临界区保护链表操作

文档以“链表”为例,展示临界区如何解决共享数据结构的同步问题,核心逻辑如下:

(1)链表结构设计

CRITICAL_SECTION变量嵌入链表结构体List中,确保每个链表实例独立受保护(而非用全局临界区锁所有链表,提升效率):

typedef struct _Node {struct _Node *next;int data;
} Node;
typedef struct _List {Node *head;                // 链表头指针CRITICAL_SECTION critical_sec; // 该链表专属的临界区变量
} List;
(2)关键操作的同步保护

对链表的CreateDeleteAddHeadInsertNext等操作,均用临界区包裹“访问链表的代码段”:

  • 创建链表:初始化链表时同时初始化临界区

    List *CreateList() {List *pList = (List *)malloc(sizeof(List));pList->head = NULL;InitializeCriticalSection(&pList->critical_sec); // 初始化临界区return pList;
    }
    
  • 插入节点:进入临界区后执行插入逻辑,避免多线程同时修改链表指针

    void AddHead(List *pList, Node *node) {EnterCriticalSection(&pList->critical_sec); // 进入临界区node->next = pList->head;  // 修改链表指针(共享操作)pList->head = node;LeaveCriticalSection(&pList->critical_sec); // 离开临界区
    }
    
  • 读取节点:即使是Next(读取node->next)也需保护——文档强调“return node->next会被编译为多个机器指令,非原子操作”,可能因线程切换导致读取错误:

    Node *Next(List *pList, Node *node) {Node* next;EnterCriticalSection(&pList->critical_sec);next = node->next; // 读取操作需同步LeaveCriticalSection(&pList->critical_sec);return next;
    }
    
(3)示例的核心优势

将临界区与链表实例绑定,而非用全局临界区,避免了“多个链表只能串行访问”的效率问题——每个链表的临界区独立,不同链表的操作可并行执行。

5. 重要特性:可重入性

文档特别指出,临界区支持可重入性:即同一个线程可以多次调用EnterCriticalSection进入同一个临界区,无需担心阻塞。

示例代码

例如Insert函数调用AddHead时,两者都需进入同一个临界区:

void Insert(List *pList, Node *afterNode, Node *newNode) {EnterCriticalSection(&pList->critical_sec); // 第1次进入if (afterNode == NULL) {AddHead(pList, newNode); // AddHead中会第2次进入同一临界区(无阻塞)} else {newNode->next = afterNode->next;afterNode->next = newNode;}LeaveCriticalSection(&pList->critical_sec); // 第1次离开
}
规则

进入次数必须与离开次数相等(如进入5次需离开5次),否则临界区会永久锁定,其他线程无法进入。

6. 使用注意事项

文档明确了多个关键禁忌和风险,需重点关注:

(1)最小化锁定时间:避免长时间占用资源
  • 核心原则:“不要长时间锁住一份资源”——临界区锁定期间,其他线程会被阻塞,若锁定时间过长(如几秒甚至分钟),会导致程序响应缓慢甚至“假死”。
  • 具体禁忌:绝对不要在临界区内调用Sleep()Wait...()类API(如WaitForSingleObject),这类函数会让线程长时间阻塞,导致临界区无法释放。
(2)避免“Dangling Critical Sections”(悬垂临界区)

临界区的最大缺点是“无法检测进入线程的状态”:若线程进入临界区后未调用LeaveCriticalSection就结束(如崩溃、调用ExitThread),会导致临界区“悬垂”,且系统无法自动清理:

  • Windows NT:该临界区会被永久锁定,后续所有尝试进入的线程都会阻塞;
  • Windows 95:后续线程会被允许进入,但此时共享资源可能处于不一致状态(如链表指针未修改完成),导致程序异常。
  • 解决方案:若需检测线程状态并自动释放同步资源,需使用后续章节的Mutex(互斥器),它是核心对象,支持检测“线程遗弃”状态。
(3)明确共享资源范围

需为每个独立的共享资源分配专属的CRITICAL_SECTION变量,而非用一个全局临界区保护所有资源——否则会导致“无关操作排队”,降低程序效率(如文档中链表示例的设计思路)。

7. 与“线程当老板”方案的对比优势

文档开头提到“让某个线程当老板”的方案(一个线程指挥所有线程排队)存在“队伍长、效率低”的缺点,而临界区的优势在于:

  • 轻量高效:无需额外线程调度,仅通过进程内变量实现同步,开销远低于“老板线程”方案;
  • 精准控制:仅锁定“处理共享资源的代码段”,而非整个线程的执行,减少不必要的阻塞;
  • 可扩展性:多个临界区可独立工作,不同共享资源的操作可并行,提升程序整体吞吐量。

六、死锁(Deadlock)

死锁是临界区等同步机制使用不当引发的严重问题,文档通过“链表交换”案例清晰阐述了其产生原因、危害及预防思路。

1. 定义与本质

文档明确:死锁是多线程因“互相持有对方所需资源、且均不释放已持资源”而陷入的“永久阻塞”状态,又称“死亡拥抱(The Deadly Embrace)”。
其本质是:当多个线程同时请求多个共享资源时,若线程间“持有资源的顺序不一致”,会形成“我等你释放、你等我释放”的循环等待,最终所有线程都无法继续执行。

2. 典型案例:SwapLists函数交换链表

文档以“用临界区保护链表,并交换两个链表内容”的场景为例,还原死锁的产生过程:

(1)案例背景
  • 每个链表(List)拥有独立的临界区(critical_sec),确保链表操作的排他性;
  • 函数SwapLists需交换两个链表的头指针(head),因此需要先后获取两个链表的临界区(先锁list1,再锁list2)。
(2)函数代码(原文示例)
void SwapLists(List *list1, List *list2)
{List *tmp_list;// 第一步:获取第一个链表的临界区EnterCriticalSection(&list1->critical_sec);// 第二步:获取第二个链表的临界区EnterCriticalSection(&list2->critical_sec);// 交换链表头指针(操作共享资源)tmp_list = list1->head;list1->head = list2->head;list2->head = tmp_list;// 释放临界区LeaveCriticalSection(&list1->critical_sec);LeaveCriticalSection(&list2->critical_sec);
}
(3)死锁产生的关键步骤

文档设定“线程A”和“线程B”同时调用SwapLists,但请求链表的顺序相反,且中途发生线程切换(context switch),具体流程如下:

步骤线程A(调用SwapLists(home, work)线程B(调用SwapLists(work, home)关键触发点
1调用EnterCriticalSection(&home->critical_sec),成功获取home链表的临界区未执行(待调度)-
2发生context switch(操作系统切换线程)开始执行线程切换是死锁的“导火索”
3被阻塞(等待CPU调度)调用EnterCriticalSection(&work->critical_sec),成功获取work链表的临界区线程B持有work临界区
4被阻塞继续调用EnterCriticalSection(&home->critical_sec),尝试获取home临界区此时home临界区已被线程A持有,线程B阻塞
5恢复执行持续阻塞(等待home临界区)-
6调用EnterCriticalSection(&work->critical_sec),尝试获取work临界区持续阻塞此时work临界区已被线程B持有,线程A阻塞
(4)最终结果
  • 线程A:持有home链表的临界区,等待work链表的临界区;
  • 线程B:持有work链表的临界区,等待home链表的临界区;
  • 两者均不释放已持有的临界区,也无法获取对方的临界区,陷入永久阻塞,即死锁。

3. 核心产生条件(基于案例提炼)

文档虽未直接列出死锁的经典“四个条件”,但案例中已隐含所有关键前提,可总结为:

  1. 多资源需求:线程需同时请求两个或更多共享资源(案例中为两个链表的临界区);
  2. 持有并等待:线程持有一个资源后,不释放,继续等待其他资源(案例中线程A持有home后等work,线程B持有work后等home);
  3. 资源不可剥夺:资源(临界区)一旦被线程持有,其他线程无法强制夺取(Win32临界区无“剥夺”机制);
  4. 循环等待:线程间形成“你等我、我等你”的循环依赖(案例中线程A↔线程B的临界区等待)。

4. 危害与处理原则

  1. 危害:死锁会导致相关线程永久阻塞,若涉及主线程或核心功能线程,会使程序“假死”(无响应),且用户无任何错误提示(文档提及“操作系统不会当掉,用户不会获得错误信息”);
  2. 处理原则:文档明确“侦测死锁的算法过于复杂”,因此预防死锁比侦测、解决死锁更实用——对大部分程序,应在设计阶段避免死锁产生,而非依赖后续“解锁”。

5. 基础预防思路(文档提及)

文档指出预防死锁的核心思路是强制资源请求的“原子性”,即“all-or-nothing(要不统统获得,要不统统没有)”:

  • 若线程需要多个资源,需确保“一次性获取所有所需资源”;若有任何一个资源无法获取,则释放已尝试获取的资源,不持有任何资源等待;
  • 针对案例中的SwapLists,可修改为:先尝试同时获取list1list2的临界区,若任一获取失败,则释放已获取的临界区,重新尝试(需结合后续章节的WaitForMultipleObjects等API实现“批量等待资源”);
  • 延伸:另一种简单预防方式是统一资源请求顺序(如约定“始终按链表地址从小到大请求临界区”),避免线程间请求顺序相反(案例中线程A和B的请求顺序相反是死锁直接原因)。

七、哲学家进餐问题(The Dining Philosophers)

“哲学家进餐问题”是文档中用来具象化死锁原理的经典案例,它与前文“SwapLists函数死锁”本质一致(均因“多资源循环等待”引发),但通过“哲学家-筷子”的生活化场景更易理解。

1. 问题的核心设定

文档明确了“哲学家进餐问题”的基础规则,是理解死锁的前提:

(1)角色与资源
  • 角色:多位哲学家围绕餐桌就坐(数量与“筷子”相等);
  • 资源:每两位哲学家之间放一支筷子(即筷子总数=哲学家总数),筷子是“共享资源”——哲学家吃饭必须同时持有左右两支筷子,且持有后不会中途放下(“不愿意在吃完之前放下筷子”)。
(2)哲学家的状态

仅三种状态:思考(不占用筷子)、等待(持有部分筷子,等待另一支)、吃饭(持有两支筷子)。

2. 死锁的产生过程(文档核心逻辑)

文档通过“哲学家拿筷子的顺序”揭示死锁的必然性,核心在于“循环等待”:

  1. 默认拿筷顺序:若每位哲学家都遵循“先拿左手边筷子,再拿右手边筷子”的规则;
  2. 死锁触发:当所有哲学家几乎同时拿起左手边的筷子(此时每位哲学家均持有1支筷子),后续尝试拿右手边筷子时,会发现右手边的筷子已被相邻的哲学家持有——此时形成“循环等待”:
    • 哲学家A持有左筷,等右筷(被哲学家B持有);
    • 哲学家B持有左筷,等右筷(被哲学家C持有);
    • ……
    • 最后一位哲学家持有左筷,等右筷(被哲学家A持有);
  3. 结果:所有哲学家均持有1支筷子、等待另一支,且无人愿意放下已持有的筷子,陷入永久阻塞(死锁),与前文“SwapLists函数死锁”的“循环等待”逻辑完全一致。

3. DINING程序:可视化演示死锁行为

文档提到“书附盘片中的DINING程序”,其核心功能是直观展示死锁的产生与预防选择,程序行为与文档描述高度绑定:

(1)程序的核心选项

程序启动时会提供两个关键选择,决定哲学家的行为模式:

  • 选择“是否使用WaitForMultipleObjects”:若选“否”(允许死锁),哲学家会“一次获取一支筷子”,持有1支时进入等待状态;若选“是”(避免死锁),哲学家会“要么一次获取两支筷子,要么什么都不获取”(避免部分持有资源)。
  • 选择“是否启用East philosophers”:若选“是”,会加速死锁的发生(文档译注提及“deadlock much faster”)。
(2)状态图例(文档明确说明)

程序通过颜色和图标区分哲学家状态,便于观察:

  • 绿色:休息(思考状态,不占用筷子);
  • 红色:等待(持有1支筷子,等待另一支);
  • 红色+两支筷子:吃饭(持有两支筷子,正常执行)。
(3)死锁的可视化表现

当程序允许死锁时,最终会出现“所有哲学家均为红色(持有1支筷子)”的状态——此时无哲学家能吃饭,完全对应前文描述的“循环等待”死锁场景。

4. 临界区的局限:为何无法解决该问题?

文档明确指出,前文的“临界区(Critical Sections)”无法解决哲学家进餐问题,核心原因与“资源等待的特性”和“临界区的本质”相关:

(1)临界区的初步尝试与缺点

文档提到一种朴素的解决思路:“使用一个临界区,允许一次只能够有一位哲学家拿起或放下筷子”——即通过临界区强制哲学家“排队拿放筷子”,避免同时拿筷导致的冲突。
但这种方法存在严重缺陷:过度限制并行性,即使哲学家之间无资源竞争(如不相邻的哲学家),也需排队等待,导致“非必要的等待”,违背多线程“提升效率”的初衷。

(2)临界区的核心局限:非核心对象,无法批量等待

文档强调,解决该问题的理想思路是“让哲学家一次尝试获取两支筷子(要么都拿到,要么都不拿)”,这需要用到上一章的WaitForMultipleObjects()函数(等待多个资源均可用时再执行)。
WaitForMultipleObjects()仅支持“核心对象”(如后续章节的Mutex),而临界区不是核心对象(无Handle,仅存在于进程内存中)——因此无法通过该函数“批量等待多个临界区”(对应FAQ17“我能够等待一个以上的critical sections吗?”的隐含答案:不能)。
这也解释了为何前文“SwapLists函数死锁”无法用临界区完美解决:临界区无法支持“多资源的原子性获取”,只能按顺序获取,进而埋下循环等待的隐患。

5. 解决方向:引入Mutex(互斥器)的必要性

文档通过该问题自然引出“临界区的不足”,并指向新的同步机制——Mutex(互斥器):

  • 核心逻辑:Mutex是Win32核心对象(有Handle),支持通过WaitForMultipleObjects()批量等待多个Mutex(对应“同时获取两支筷子”的需求);
  • 解决死锁的关键:若用“每支筷子对应一个Mutex”,哲学家可通过WaitForMultipleObjects()同时等待左右两支筷子的Mutex——仅当两支都可用时才获取,否则不持有任何一个,从根本上避免“部分持有资源”和“循环等待”(对应FAQ16“我如何避免死锁?”的核心答案:通过“all-or-nothing”原则,要么获取所有所需资源,要么不获取)。
    这也成为后续章节讲解“Mutex(互斥器)”的重要铺垫,体现了文档“从问题到解决方案”的递进逻辑。

八、互斥器(Mutexes)

互斥器(Mutex,全称Mutual Exclusion)是Win32中支持互斥访问、具备跨进程能力且弹性更强的同步机制,核心作用与临界区一致——确保同一时间仅一个线程访问共享资源,但在速度、跨进程支持、等待控制等方面有显著差异。

1. Mutex与Critical Section的核心区别

文档明确两者“功能类似但弹性不同”,核心差异体现在4个维度:

对比维度互斥器(Mutex)临界区(Critical Section)
速度性能慢(锁住未被拥有的Mutex比临界区慢约100倍)
原因:需进入操作系统内核态(kernel mode)操作,而临界区在用户态(user mode/Win95的ring3)即可完成
快(用户态操作,无需内核介入)
跨进程能力支持(需通过CreateMutex指定全局唯一名称,其他进程可通过名称访问)不支持(仅存在于当前进程内存空间,无Handle)
等待超时控制支持(可通过WaitForSingleObject等函数指定等待时间,超时返回WAIT_TIMEOUT不支持(一旦进入等待,只能等资源释放,无法主动超时退出)
核心对象属性是Win32核心对象,有Handle,需通过CloseHandle管理引用计数非核心对象,无Handle,仅通过CRITICAL_SECTION变量管理
补充:函数对应关系表(文档原文整理)

为便于对比使用,文档给出两者的API映射关系,明确“替换临界区时需改用哪些Mutex函数”:

临界区(Critical Section)函数互斥器(Mutex)相关函数
InitializeCriticalSectionCreateMutex(创建/初始化)
-OpenMutex(打开已存在的Mutex,跨进程用)
EnterCriticalSectionWaitForSingleObject/WaitForMultipleObjects/MsgWaitForMultipleObjects(获取拥有权)
LeaveCriticalSectionReleaseMutex(释放拥有权)
DeleteCriticalSectionCloseHandle(减少引用计数,计数为0时销毁)

2. Mutex的核心API与使用流程

Mutex作为核心对象,需通过标准“创建/打开→获取拥有权→操作资源→释放→销毁”流程使用:

(1)产生Mutex:CreateMutex

用于创建新Mutex或获取已存在Mutex的Handle,是使用Mutex的起点。

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,  // 安全属性(Win95无效,NULL为默认)BOOL bInitialOwner,                       // 初始拥有者:TRUE=创建线程拥有,FALSE=无初始拥有者LPCTSTR lpName                            // 名称(跨进程用,不含反斜线,需全局唯一)
);
关键说明:
  • 参数lpName:跨进程同步的核心——若指定名称,其他进程可通过该名称打开此Mutex;若为NULL,则仅当前进程内线程可用。名称需唯一(如“Company.AppName.Mutex”),避免与其他程序冲突。
  • 返回值与错误处理
    • 成功:返回Mutex的Handle;
    • lpName已存在:返回该Mutex的Handle,但GetLastError()会传回ERROR_ALREADY_EXISTS(非错误,仅表示未创建新对象);
    • 失败:返回NULL,需通过GetLastError()获取具体原因。
  • 示例(文档原文):创建一个无安全属性、无初始拥有者、名为“Demonstration Mutex”的Mutex:
    HANDLE hMutex;
    void CreateAndDeleteMutex() {hMutex = CreateMutex(NULL, FALSE, "Demonstration Mutex");/* 操作共享资源 */CloseHandle(hMutex);  // 释放引用,计数为0时销毁
    }
    
(2)打开已存在的Mutex:OpenMutexCreateMutex

当Mutex已被其他进程创建(如服务器进程创建,客户端进程需访问),可通过两种方式获取Handle:

  • OpenMutex:专门用于打开已存在的命名Mutex(推荐,语义更清晰),需指定Mutex名称和访问权限;
  • CreateMutex:若指定的lpName已存在,会直接返回该Mutex的Handle(而非创建新对象),与OpenMutex效果一致,但需通过GetLastError()判断是否为新创建。
适用场景:

客户端-服务器架构中,服务器创建Mutex保护共享资源,客户端无需创建,仅需打开即可同步(如多个客户端访问服务器的共享配置文件)。

(3)锁住Mutex(获取拥有权):Wait系列函数

Mutex的“锁住”本质是获取其拥有权,需通过WaitForSingleObjectWaitForMultipleObjects等函数实现,核心逻辑与“激发状态”绑定:

① Mutex的激发状态规则(文档核心)
  • 激发状态:无线程拥有Mutex时,Mutex处于“激发状态”,此时Wait函数会立即返回,且返回后Mutex会立刻转为非激发状态(防止其他线程同时获取);
  • 非激发状态:有线程拥有Mutex时,Mutex处于“非激发状态”,其他线程调用Wait会阻塞,直到拥有者释放Mutex。
② 等待流程示例(文档描述)
  1. Mutex无拥有者(激发状态);
  2. 线程A调用WaitForSingleObject(hMutex, INFINITE),Wait函数检测到激发状态,返回WAIT_OBJECT_0,同时Mutex转为非激发状态;
  3. 线程B调用WaitForSingleObject(hMutex, INFINITE),因Mutex非激发,线程B阻塞;
  4. 线程A调用ReleaseMutex(hMutex),Mutex转回激发状态;
  5. 线程B的Wait函数检测到激发,返回WAIT_OBJECT_0,Mutex再次转为非激发状态。
③ 关键API参数

WaitForMultipleObjects为例(解决多资源等待,如哲学家问题):

DWORD WaitForMultipleObjects(DWORD nCount,          // 等待的Handle数量(如2支筷子→2)CONST HANDLE *lpHandles,// Handle数组(如存放两支筷子的Mutex Handle)BOOL bWaitAll,         // TRUE=所有Handle激发才返回(关键,避免部分拥有)DWORD dwMilliseconds   // 等待时间(INFINITE=无限等待)
);
  • 文档中“哲学家问题”即通过bWaitAll=TRUE实现“要么同时获取两支筷子,要么都不获取”,从根本上避免死锁。
(4)释放Mutex(释放拥有权):ReleaseMutex

拥有Mutex的线程可释放,函数原型:

BOOL ReleaseMutex(HANDLE hMutex);  // 参数:需释放的Mutex Handle
关键注意点:
  • 拥有权归属:Mutex的拥有权属于“最后调用Wait且未释放的线程”,而非“创建Mutex的线程”——即使线程B创建Mutex,线程A调用Wait后也会成为拥有者;
  • 释放次数匹配:若一个线程多次调用Wait获取同一Mutex(可重入),需调用相同次数的ReleaseMutex才能完全释放(与临界区的Enter/Leave次数匹配规则一致)。
(5)销毁Mutex:CloseHandle

Mutex作为核心对象,通过引用计数管理生命周期:

  • 每次调用CreateMutexOpenMutex,引用计数+1;
  • 每次调用CloseHandle,引用计数-1;
  • 当引用计数降至0时,Mutex被操作系统自动销毁(与线程是否结束无关,除非线程是最后一个持有Handle的)。
注意:

即使线程拥有Mutex但未释放就结束,只要其他线程仍持有该Mutex的Handle,Mutex也不会销毁(仅拥有权被释放)。

3. Mutex的特殊特性:处理“被舍弃的Mutex”

文档强调,这是Mutex独有的、区别于临界区的核心特性——解决“线程持有Mutex未释放就结束”的问题:

(1)被舍弃的场景

线程A持有Mutex(未释放),因异常(如崩溃)或调用ExitThread结束,此时Mutex不会被销毁,而是触发特殊处理:

  • Mutex被标记为“未被拥有”(但非激发状态);
  • 下一个等待该Mutex的线程(如线程B)调用Wait时,会返回WAIT_ABANDONED_0(单Mutex)或WAIT_ABANDONED_0 ~ WAIT_ABANDONED_0+nCount-1(多Mutex),通知“前一个拥有者未释放就结束”。
(2)应对建议
  • 通知的意义:告知当前线程“共享资源可能处于不一致状态”(前一个线程可能未完成数据修改);
  • 处理方式:需检查共享资源的完整性(如链表是否断裂、文件是否完整),避免直接使用损坏的数据。

4. Mutex的实际应用:两个经典案例

文档通过“哲学家进餐问题”和“修正SwapLists函数”,展示Mutex如何解决之前临界区无法处理的死锁问题:

(1)案例1:哲学家进餐问题(解决死锁)
场景回顾:

哲学家需同时持有左右两支筷子(共享资源),用临界区时因“部分持有”导致死锁;改用Mutex后:

  • 资源映射:每支筷子对应一个Mutex(gChopStick[i] = CreateMutex(NULL, FALSE, NULL));
  • 等待逻辑:哲学家调用WaitForMultipleObjects(2, myChopsticks, TRUE, INFINITE)bWaitAll=TRUE确保“要么同时获取两支筷子,要么都不获取”;
  • 效果:无线程会“持有一支筷子等待另一支”,彻底避免循环等待,死锁不再发生。
(2)案例2:修正SwapLists函数(解决循环等待)
问题回顾:

用临界区时,线程A锁homework,线程B锁workhome,形成死锁;改用Mutex后:

  • 结构修改List结构体中用HANDLE hMutex代替CRITICAL_SECTION
  • 同步逻辑:通过WaitForMultipleObjects同时等待两个链表的Mutex,bWaitAll=TRUE确保“要么同时锁住两个链表,要么都不锁”;
  • 修正代码(文档列表4-2核心)
    void SwapLists(struct List *list1, struct List *list2) {struct List *tmp_list;HANDLE arrhandles[2] = {list1->hMutex, list2->hMutex};// 同时等待两个Mutex,均激发才返回WaitForMultipleObjects(2, arrhandles, TRUE, INFINITE);// 交换链表头(安全操作)tmp_list = list1->head;list1->head = list2->head;list2->head = tmp_list;// 释放MutexReleaseMutex(arrhandles[0]);ReleaseMutex(arrhandles[1]);
    }
    
  • 效果:线程A和B均需同时获取两个Mutex才能执行交换,不会出现“部分持有”,循环等待被打破。

5. Mutex的适用场景总结

结合文档内容,Mutex适合以下场景:

  1. 跨进程同步:需多个进程共享资源(如多个程序访问同一配置文件);
  2. 需等待超时:避免线程永久阻塞(如等待Mutex 5秒后超时退出,提升程序容错性);
  3. 处理线程异常结束:通过WAIT_ABANDONED_0感知前线程未释放资源,保护数据完整性;
  4. 多资源同步:需同时获取多个共享资源(如哲学家、SwapLists),通过WaitForMultipleObjects避免死锁。

而临界区更适合“同一进程内、对速度要求高、仅需简单互斥”的场景(如进程内多个线程操作全局数组)。

九、信号量(Semaphores)

信号量(Semaphores)是Win32中用于解决“多个线程共享有限数量资源” 的经典同步机制,核心作用是通过“资源计数”确保线程不会超出资源总量访问共享资源。

1. 核心定位与作用

(1)本质:有限资源的“计数器”

信号量的核心是维护一个“可用资源数量”的计数器,通过“计数增减”控制线程对资源的访问:

  • 当线程需要访问资源时,计数器减1(锁定资源);
  • 当线程释放资源时,计数器加1(释放资源);
  • 当计数器为0时,后续线程需阻塞等待,直到有资源被释放(计数器>0)。

文档强调:信号量是解决Producer/Consumer(生产者-消费者)问题的关键——例如“环状缓冲区”(生产者写入数据、消费者读取数据),缓冲区大小有限,需通过信号量控制“可读数据量”,避免消费者读取空缓冲区或生产者写满缓冲区。

(2)生活化实例:租车店的敞篷车(文档核心例子)

文档用“租车店租敞篷车”的场景,直观解释信号量的作用:

  • 资源:租车店有3辆敞篷车(有限资源,数量=3);
  • 线程:4位客户(对应4个线程)同时要租敞篷车,由租车代理人(线程操作)处理;
  • 问题:若无同步机制,可能出现“同一辆车被租给2个客户”(资源重复分配);
  • 信号量解决方案
    1. 用信号量维护“可用敞篷车数量”,初始值=3(lInitialCount=3),最大值=3(lMaximumCount=3);
    2. 代理人处理租车时,调用Wait函数——信号量现值减1(可用车数-1);
    3. 客户还车时,调用ReleaseSemaphore——信号量现值加1(可用车数+1);
    4. 当4个客户同时租车时,前3个客户成功(现值从3→0),第4个客户阻塞,直到有客户还车(现值>0)。

这个例子的核心是:信号量通过“不可分割的计数操作”,避免“有限资源被超额分配”,且无需为每个资源单独创建同步对象(如Mutex),减少系统开销。

2. 信号量与Mutex的核心区别(文档重点对比)

文档明确:Mutex是信号量的“退化形式”(当信号量的lMaximumCount=1时,即为“二元信号量”,类似Mutex),但Win32中两者无法互换,核心差异体现在4个维度:

对比维度信号量(Semaphore)互斥器(Mutex)
资源数量支持支持“有限多个资源”(如3辆敞篷车),最大值由lMaximumCount指定仅支持“1个资源”(独占访问),本质是“二元信号量”
拥有权归属无“拥有权”概念——任何线程均可调用ReleaseSemaphore释放,无需是“获取锁定的线程”有“拥有权”——仅“最后调用Wait且未释放的线程”可释放(ReleaseMutex
“线程异常结束”处理无“Wait Abandoned”状态——若线程获取锁定后未释放就结束,信号量计数器不会自动恢复,可能导致资源泄漏有“Wait Abandoned”状态——线程未释放就结束,下一个Wait的线程会收到通知,Mutex自动转为可获取状态
典型适用场景有限资源共享(如连接池、缓冲区、租车店车辆)独占资源访问(如单一链表、全局变量)

3. 核心API与使用流程

文档详细说明信号量的3个核心操作:创建→获取锁定→释放锁定,需严格遵循“计数增减”逻辑:

(1)创建信号量:CreateSemaphore

用于创建新信号量或获取已存在信号量的Handle,是使用信号量的起点。

HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpAttributes,  // 安全属性(Win95无效,NULL=默认)LONG lInitialCount,                  // 信号量初值(≥0且≤lMaximumCount)LONG lMaximumCount,                 // 信号量最大值(最大可用资源数)LPCTSTR lpName                       // 名称(跨进程用,NULL=仅当前进程可用)
);
关键参数解析:
  • lInitialCount(初值):初始可用资源数。例如租车店初始有3辆敞篷车→lInitialCount=3;若需先初始化资源(如环状缓冲区),可设为0,初始化后再通过ReleaseSemaphore增加。
  • lMaximumCount(最大值):资源总量上限,不可突破。例如租车店最多3辆敞篷车→lMaximumCount=3,即使调用ReleaseSemaphore也无法让现值超过3。
  • lpName(名称):支持跨进程同步——若指定全局唯一名称(如“Company.CarRental.Semaphore”),其他进程可通过该名称打开信号量;若为NULL,仅当前进程内线程可用。
返回值与错误处理:
  • 成功:返回信号量Handle;
  • lpName已存在:返回该信号量的Handle,GetLastError()传回ERROR_ALREADY_EXISTS(非错误,仅表示未创建新对象);
  • 失败:返回NULL,需通过GetLastError()获取原因。
(2)获取锁定:通过Wait系列函数

线程需通过WaitForSingleObjectWaitForMultipleObjects等函数“申请资源”,本质是将信号量现值减1

核心逻辑:
  • 若信号量现值>0Wait函数立即返回WAIT_OBJECT_0,同时现值减1(资源被占用);
  • 若信号量现值=0Wait函数阻塞,直到其他线程释放信号量(现值>0)。
关键特性:
  • 无“拥有权”限制:同一线程可多次调用Wait函数,每次调用都会让现值减1(只要现值够)——例如线程A调用2次Wait,若初始现值=3,调用后现值=1,无需担心阻塞(与Mutex不同,Mutex拥有者多次Wait不会阻塞,但不改变计数)。
(3)释放锁定:ReleaseSemaphore

线程释放资源时,需调用此函数将信号量现值增加指定数值,函数原型:

BOOL ReleaseSemaphore(HANDLE hSemaphore,         // 需释放的信号量HandleLONG lReleaseCount,        // 现值增加量(必须>0,不可为负或0)LPLONG lpPreviousCount     // 输出参数:返回释放前的现值(可选,NULL=不获取)
);
关键规则:
  • 现值不超限:增加后的现值绝对不会超过CreateSemaphore指定的lMaximumCount——例如最大值=3,当前现值=2,lReleaseCount=2,则最终现值=3(而非4)。
  • 无拥有权限制:任何线程均可调用,无需是“获取锁定的线程”——例如线程A获取锁定(现值减1),线程B可调用ReleaseSemaphore释放(现值加1),这与Mutex的“仅拥有者释放”完全不同。
示例(基于租车场景):
// 1. 创建信号量:3辆敞篷车(初值3,最大值3)
HANDLE hCarSemaphore = CreateSemaphore(NULL, 3, 3, "CarRental.Convertible.Semaphore");
// 2. 线程A(租车):获取锁定,现值3→2
WaitForSingleObject(hCarSemaphore, INFINITE);
// 3. 线程B(还车):释放锁定,现值2→3
ReleaseSemaphore(hCarSemaphore, 1, NULL);
// 4. 销毁信号量
CloseHandle(hCarSemaphore);

4. 信号量初值(lInitialCount)的意义

文档特别解释:初值的设定与“资源初始化”强相关,核心目的是确保资源准备就绪后再开放访问

  • 若初值设为0:初始时信号量现值=0,所有调用Wait的线程都会阻塞——适合“先初始化资源,再开放访问”的场景,例如环状缓冲区:
    1. 创建信号量(初值0,最大值=缓冲区大小);
    2. 生产者线程初始化缓冲区(如分配内存、清空数据);
    3. 调用ReleaseSemaphore将现值设为缓冲区大小(或初始可用数据量),允许消费者线程访问。
  • 若初值设为最大值:初始时资源全部可用,适合“资源已就绪”的场景(如租车店一开始就有3辆敞篷车)。

5. 典型应用:生产者-消费者问题

文档明确信号量是解决“生产者-消费者”问题的关键,以“环状缓冲区”为例:

  • 生产者(Producer):向缓冲区写入数据,每次写成功后调用ReleaseSemaphore(现值+1,代表可用数据量增加);
  • 消费者(Consumer):从缓冲区读取数据,读取前调用WaitForSingleObject(现值-1,代表可用数据量减少),若现值=0则阻塞,直到生产者写入数据;
  • 信号量作用:通过“可用数据量计数”,避免消费者读取空缓冲区、或生产者写入满缓冲区(需配合另一个信号量控制“空闲空间”,文档未展开,但核心逻辑一致)。

6. 适用场景总结

结合文档内容,信号量适合以下场景:

  1. 有限资源共享:如数据库连接池(10个连接)、硬件设备(2个打印机)、租车店车辆等;
  2. 生产者-消费者模型:如环状缓冲区、消息队列,需控制“可用数据/空闲空间”数量;
  3. 跨进程同步:需多个进程共享有限资源(如多个程序访问同一批临时文件),通过命名信号量实现。
    而Mutex更适合“独占资源访问”(如单一数据结构、全局变量),两者需根据资源数量和访问需求选择,不可互换。

十、事件(Event Objects)与互锁变量(Interlocked Variables)

这两部分是Win32同步机制的重要补充——Event Objects解决“线程间状态通知”问题,Interlocked Variables解决“32位变量原子操作”问题。

一、事件(Event Objects):完全可控的核心同步对象

Event Objects是Win32中最具弹性的同步机制,核心特点是“状态完全由程序控制”,不受Wait...()函数副作用影响(区别于Mutex、Semaphore),仅用于“通知线程某个条件成立”(如I/O完成、资源就绪)。

1. 核心定位:状态可控的“信号开关”

Event的本质是一个“二元状态开关”,只有两种状态:

  • 激发状态(Signaled):等待该Event的线程会被唤醒;
  • 非激发状态(Non-Signaled):等待该Event的线程会阻塞。

关键优势:状态变化完全由程序主动控制——Mutex/Semaphore的状态会被Wait函数自动改变(如Mutex的Wait会让其变非激发),而Event的状态仅通过SetEvent/ResetEvent/PulseEvent修改,Wait函数只“检测状态”不“改变状态”,这是其“弹性”的核心来源。

2. 创建Event:CreateEvent函数与关键参数

通过CreateEvent创建Event对象,函数原型及参数决定了Event的核心行为:

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,  // 安全属性(Win95无效,NULL=默认)BOOL bManualReset,                       // 重置方式:自动/手动BOOL bInitialState,                      // 初始状态:激发(TRUE)/非激发(FALSE)LPCTSTR lpName                           // 名称(跨进程用,NULL=仅当前进程)
);
核心参数解析(决定Event行为的关键):
参数名取值与意义
bManualReset- FALSE(AutoReset Event,自动重置):Event被激发后,唤醒一个等待线程,随后自动转为非激发状态(无需程序干预);
- TRUE(ManualReset Event,手动重置):Event被激发后,唤醒所有等待线程,保持激发状态,需调用ResetEvent手动转为非激发状态。
bInitialState- TRUE:Event创建后初始为激发状态;
- FALSE:初始为非激发状态(默认常用)。
返回值与错误处理:
  • 成功:返回Event的Handle;
  • lpName已存在:返回已有Event的Handle,GetLastError()传回ERROR_ALREADY_EXISTS(非错误,仅表示未创建新对象);
  • 失败:返回NULL,需通过GetLastError()获取原因。
3. Event的核心操作函数

文档明确了3个控制Event状态的函数,需结合“AutoReset/ManualReset”类型区分行为:

函数名功能对AutoReset/ManualReset的差异
SetEvent将Event设为激发状态- AutoReset:激发后,等待线程被唤醒一个,自动变非激发;
- ManualReset:激发后,所有等待线程被唤醒,保持激发直到ResetEvent
ResetEvent将Event设为非激发状态- AutoReset:无效(因AutoReset会自动变非激发,手动调用无意义);
- ManualReset:有效(必须调用此函数才能让激发状态的Event变非激发)。
PulseEvent短暂将Event设为激发,随后自动重置为非激发(“脉冲式”信号)- AutoReset:激发→唤醒1个线程→自动变非激发;
- ManualReset:激发→唤醒所有线程→自动变非激发。
4. EVENTTST程序:可视化演示Event行为

文档提到的EVENTTST程序是理解Event的关键实例,核心逻辑是“3个线程等待1个Event,用户通过按钮控制Event状态”,不同类型的Event表现差异显著:

(1)AutoReset Event(bManualReset=FALSE
  • 操作【ResetEvent】:无效果(AutoReset本身会自动非激发,手动重置无用);
  • 操作【SetEvent】:Event激发→唤醒1个等待线程→自动变非激发;
  • 操作【PulseEvent】:Event短暂激发→唤醒1个等待线程→自动变非激发;
  • 特点:每次仅唤醒1个线程,避免线程“争抢”。
(2)ManualReset Event(bManualReset=TRUE
  • 操作【ResetEvent】:Event从激发→非激发(仅手动操作有效);
  • 操作【SetEvent】:Event激发→唤醒所有等待线程→保持激发(需手动Reset才变非激发);
  • 操作【PulseEvent】:Event激发→唤醒所有等待线程→自动变非激发;
  • 特点:适合“通知所有线程某个条件成立”(如“任务开始”“资源就绪”)。
程序额外启示:OS的线程公平性

文档强调:OS会保证等待中的线程“轮番被唤醒”,避免某个线程长期得不到CPU(“饥饿(Starvation)”)——这是所有Win32同步机制的共同特性,确保线程调度的公平性。

5. Event的关键局限性:信号遗失

文档明确Event的致命缺点:无等待线程时,激发信号会遗失——Event不保存“激发历史”,仅当线程正在等待时,激发信号才有效;若先激发Event(如PulseEvent),再让线程等待,信号会丢失,可能导致死锁。

示例(文档描述的死锁场景):
  1. 线程A(接收者):检查队列→发生context switch
  2. 线程B(发送者):对Event调用PulseEvent(此时无线程等待,信号遗失);
  3. 线程A恢复执行:调用WaitForSingleObject等待Event→因信号已遗失,永久阻塞→死锁。
    这也是Semaphore能解决此问题的原因:Semaphore通过“计数”保存资源状态(即使无等待线程,计数仍会累积),而Event无计数,仅“即时响应”。

二、互锁变量(Interlocked Variables):轻量的32位变量原子操作

Interlocked Variables是Win32中最轻量的同步机制,无“等待”功能,仅用于保证“32位变量的原子操作”,避免因“非原子的加减/赋值”导致的race condition(如多线程同时修改计数器)。

1. 核心定位:解决“简单变量的race condition”

普通32位变量的“加减/赋值”操作会被编译为多个机器指令(如“读变量→修改→写回”),若线程在中间切换,会导致数据错误(如线程A读计数器=5,线程B也读=5,都加1后写回=6,实际应=7)。
Interlocked Variables通过硬件支持的原子操作,确保“读-改-写”过程不可分割,无需Critical Section/Mutex等重型同步,效率极高。

2. 核心函数:3个原子操作函数

文档介绍了3个核心函数,均针对“32位long变量”(地址需对齐为long word):

(1)InterlockedIncrement:原子加1
LONG InterlockedIncrement(LPLONG lpTarget);  // lpTarget:32位变量地址
  • 功能:将lpTarget指向的变量原子加1
  • 返回值:加1后的变量值(与0比较的结果:>0返回正值,=0返回0,<0返回负值);
  • 用途:引用计数增加(如OLE对象的AddRef()函数)。
(2)InterlockedDecrement:原子减1
LONG InterlockedDecrement(LPLONG lpTarget);
  • 功能:将lpTarget指向的变量原子减1
  • 返回值:减1后的变量值(与0比较,同上);
  • 用途:引用计数减少(如OLE对象的Release()函数)——当返回0时,说明对象无引用,可销毁。
(3)InterlockedExchange:原子赋值并返回旧值
LONG InterlockedExchange(LPLONG lpTarget, LONG lValue);
  • 功能:将lValue原子赋值lpTarget指向的变量,同时返回变量的旧值
  • 用途:原子更新标志位(如“将状态从‘空闲’设为‘忙碌’,并检查旧状态是否为空闲”)。
3. 适用场景与优势
(1)核心适用场景
  • 引用计数(Reference Counting):如系统核心对“核心对象Handle”的计数、OLE对象的AddRef()/Release()——需确保计数增减的原子性,避免对象提前销毁或内存泄漏;
  • 简单标志位更新:如“线程状态(运行/停止)”“资源是否可用”等32位变量的原子修改。
(2)与其他同步机制的区别
对比维度Interlocked VariablesCritical Section/Mutex
功能仅32位变量的原子操作,无等待机制保护代码块,支持等待(阻塞线程)
开销极轻(硬件原子操作,无内核/用户态切换)较重(Critical Section用户态切换,Mutex内核态切换)
适用场景简单变量(计数器、标志位)复杂代码块、共享资源(链表、文件)
4. 关键注意事项
  • 变量类型限制:仅支持32位long变量(地址需对齐为long word,否则可能出错);
  • 无等待功能:若需“等待变量达到某个值”(如等待计数器=0),需结合Event/Semaphore等机制,Interlocked函数本身无法阻塞线程;
  • 硬件依赖:原子操作由CPU硬件支持(如Pentium的LOCK指令),确保多CPU(SMP)环境下仍有效。

三、总结:两种机制的选型建议

机制核心用途选型依据
Event Objects线程间状态通知(如I/O完成、任务开始/结束)需“灵活控制信号”“通知1个/所有线程”,且能接受“信号遗失”风险(或搭配其他机制规避);跨进程同步也适用。
Interlocked Variables32位变量原子操作(计数器、标志位)仅需简单变量的加减/赋值同步,追求轻量高效,无需等待功能。

📣 结语

感谢你耐心看完Win32多线程同步控制的核心内容!本章从“为什么需要同步”到“具体同步工具的使用”,一步步带你理解了临界区、死锁、互斥器、信号量、事件和互锁变量的核心逻辑,所有例子和比喻都保留了文档的原貌,希望能帮零基础的你打好基础。

如果你觉得我写的不错,记得给我点赞,收藏 和 关注哦(。・ω・。)

让我们一起加油,向美好的未来奔去。让我们从一无所知的新手逐渐成为专家。为自己点赞吧!

http://www.dtcms.com/a/491485.html

相关文章:

  • 2024.6卷一阅读短语
  • 企业营销推广型网站建设怎么创造软件app
  • Rust 的错误处理:别拿类型系统当护身符
  • 用栈实现记忆存储——C++语言自制时间计算器
  • 实验二 呼吸灯功能实验
  • 动力 网站建设珠海专业网站建设费用
  • 博客系统测试
  • 高德地图电子围栏/地图选区/地图打点
  • 自己动手建设网站过程dede珠宝商城网站源码
  • Git的分支
  • 基础拓展
  • 手机微网站建设河南网站建设的详细策划
  • 剧本杀小程序系统开发:内容生态与商业模式的双轮驱动
  • 网站备案表不会写引流网站怎么做
  • 【系统分析师】写作框架:数据灾务技术与应用
  • 香港云服务器域名无法访问的原因
  • 荆门网站建设服务上海网站制作网络推广方法
  • 系统那个网站好什么是网站平台开发
  • 网站管理后台地址深圳做网站排名
  • 软件测试及 AI+测试
  • oj字符串,求助讨论帖
  • 鸿蒙app开发中 class类中的 访问修饰符和静态修饰符 等这些命名的含义 以及用法
  • 大模型-AIGC技术在文本生成与音频生成领域的应用
  • 国内产品网站1688利用腾讯云建设网站
  • 下载免费网站模板下载移动网站建设机构
  • 公司网站怎么建温泉网站建设
  • error: can‘t find Rust compiler
  • 关于力扣第 167 场双周赛的赛后总结 第三四题
  • 网站开发h5技术两学一做网站源码
  • SpringBoot-自动配置原理