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

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

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

🥇 专栏:

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

文章目录

    • 📚 前言
    • 一. 认识volatile关键字:阻止编译器“好心办坏事”
      • 1. 编译器优化的“坑”:寄存器里的“过期数据”
      • 2. volatile关键字的作用:标记“易变”变量
      • 3. volatile的使用实例:从代码看差异
        • 实例1:未加volatile的函数(有问题)
        • 实例2:加了volatile的函数(正确)
      • 4. volatile的进阶用法:与const结合
    • 二. Referential Integrity:多线程中的“数据关联性”保护
      • 1. 从数据库事务理解“参照完整性”
      • 2. 多线程中的“参照完整性”问题
        • 例子1:数组附加数据的隐患
        • 例子2:删除形状的“指针悬空”问题
      • 3. 解决方法:把锁定“提级”
    • 三. The Readers/Writers Lock:平衡“读多写少”场景的效率
      • 1. 排他锁定的“效率瓶颈”
      • 2. 借鉴OpenFile的共享模式
      • 3. Readers/Writers Lock的核心算法
        • 读锁的获取与释放
        • 写锁的获取与释放
      • 4. 读写锁的Win32实现:数据结构与核心函数
        • 步骤1:定义RWLock数据结构
        • 步骤2:初始化读写锁(InitRWLock)
        • 步骤3:获取与释放读锁
        • 步骤4:获取与释放写锁
      • 5. 范例程序:READWRIT的作用
    • 四. 我需要锁定吗?:判断锁定的“黄金法则”
    • 五. Lock Granularity:锁定粒度的“选择艺术”
      • 1. 两种粒度的对比
      • 2. 粒度选择建议
    • 📣 结语

📚 前言

在Win32多线程程序中,“数据一致性”是绕不开的核心问题。想象一下:多个线程像一群人同时编辑同一份文档,有人改标题、有人删段落、有人加内容,若不加以约束,文档很快会变得混乱——这就是多线程共享数据时的真实场景。本章(《Win32多线程程序设计》第七章)的核心目标,就是教你如何避免这种“混乱”,通过volatile关键字、Readers/Writers Lock(读写锁)等工具,确保多线程操作数据时的“正确性”和“效率”平衡。即使你是零基础,也能通过本章的比喻和实例,逐步理解多线程数据保护的逻辑。

一. 认识volatile关键字:阻止编译器“好心办坏事”

1. 编译器优化的“坑”:寄存器里的“过期数据”

编译器为了让程序跑得更快,会做一个“优化”:把频繁使用的变量拷贝到CPU的寄存器(相当于你手边的“通讯录”)里——从寄存器读数据比从内存读快得多。但这个“好心”在多线程中会出大问题:

假设线程A把变量count(存着用户数量)拷贝到寄存器,线程B却修改了内存中count的值(比如新增了一个用户)。此时线程A的寄存器里,count还是旧值,就像你通讯录里的电话没更新,打过去找的还是“旧用户”——这就是“数据过期”问题。

单线程中不会有这个问题(编译器能跟踪变量的所有修改),但多线程中编译器无法知道其他线程在做什么,所以必须“阻止”它把共享变量放进寄存器。

2. volatile关键字的作用:标记“易变”变量

volatile是C/C++的标准关键字(所有编译器都支持),它的核心作用是告诉编译器:这个变量可能被其他线程修改,不要把它的临时拷贝放在寄存器里,每次用都要从内存重新读

  • 适用场景:所有被多个线程共享的变量(比如全局变量、跨线程传递的指针指向的变量)。
  • 注意:volatile不替代临界区、互斥器等同步工具,它只解决“编译器优化导致的读取过期”问题,不解决“多线程同时修改”的竞争问题。

3. volatile的使用实例:从代码看差异

我们用一个简单的WaitForKey函数(等待变量pch从0变成非0),看volatile的影响:

实例1:未加volatile的函数(有问题)
// 未加volatile:编译器可能把*pch放进寄存器
void WaitForKey(char *pch) {while (*pch == 0); // 循环检查*pch是否为0
}

编译器优化后的汇编代码(关键部分):

; 把pch指向的地址放进eax寄存器
mov eax, DWORD PTR _pch$[esp-4]
; 把*pch的值放进al寄存器(只读一次!)
movsx al, BYTE PTR [eax]
$L84:
; 只检查al寄存器的值(不读内存了!)
test al, al
je SHORT $L84 ; 如果al是0,继续循环

问题:一旦*pch被其他线程改成非0,线程A的循环还是会继续——因为它只检查寄存器里的旧值。

实例2:加了volatile的函数(正确)
// 加volatile:告诉编译器*pch可能被其他线程修改
void WaitForKey(volatile char *pch) {while (*pch == 0);
}

编译器优化后的汇编代码(关键部分):

; 把pch指向的地址放进eax寄存器(地址不变,只读一次)
mov eax, DWORD PTR _pch$[esp-4]
$L84:
; 每次循环都从内存读*pch的值(不依赖寄存器)
cmp BYTE PTR [eax], 0
je SHORT $L84 ; 如果内存中的值是0,继续循环

正确:每次循环都从内存重新读取*pch,其他线程修改后能立刻感知。

4. volatile的进阶用法:与const结合

如果一个变量“不能被当前函数修改,但可能被其他线程修改”,可以用const volatile修饰:

// 函数不能改value,但其他线程可能改
void PrintValue(const volatile int *value) {printf("%d\n", *value); // 每次读都从内存取最新值
}

二. Referential Integrity:多线程中的“数据关联性”保护

1. 从数据库事务理解“参照完整性”

在数据库中,“参照完整性”(Referential Integrity)指“一组改变必须全部完成,否则数据会不一致”。比如:

  • 销售数据库里,“发票记录”和“采购项目记录”是关联的——不能只新增发票、不填采购项目,也不能只删采购项目、保留发票。
  • 这种“要么全成、要么全不做”的操作叫“事务”(Transaction),数据库会暂存所有修改,直到事务完成才一次性写入,避免中间状态被读取。

2. 多线程中的“参照完整性”问题

多线程程序也有同样的问题:一个操作可能涉及多个数据的修改,若修改到一半被其他线程打断,数据就会“残缺”。

例子1:数组附加数据的隐患

要在数组末尾加一条数据,需要两步:

  1. 把数据写入数组的空闲位置;
  2. 把数组的“元素计数”加1。

如果线程A执行完第一步后,线程B读取数组:此时数组里有新数据,但计数没更新——线程B会以为“新数据不存在”,导致错误。

例子2:删除形状的“指针悬空”问题

假设绘图软件有两个线程:

  • 线程1(UI线程):管理形状数组,提供GetShapePointer(获取形状指针)和DeleteShape(删除形状)函数;
  • 线程2(绘图线程):用GetShapePointer拿到形状指针后绘图。

若线程2拿到指针后,线程1调用DeleteShape释放了该形状的内存——线程2的指针就会指向“无效内存”(悬空指针),程序可能崩溃。

3. 解决方法:把锁定“提级”

要保证数据关联性,需要把“分散的小锁定”改成“集中的大锁定”——在修改一组关联数据前,先锁定整个数据结构,直到所有修改完成再释放。

比如之前的AddLineItems函数(给链表加多个元素),原本每个AddHead都单独加锁,现在把锁提到AddLineItems层面:

// 改进前:每个AddHead单独加锁(有漏洞)
void AddLineItems(List *pList) {Node node;while (还有元素要加) {GetLineItem(&node);AddHead(pList, &node); // AddHead里有Enter/LeaveCriticalSection}
}// 改进后:整个循环加锁(无漏洞)
void AddLineItems(List *pList) {Node node;// 锁定整个链表,直到所有元素加完EnterCriticalSection(&pList->critical_sec);while (还有元素要加) {GetLineItem(&node);AddHead(pList, &node); // 不用再单独加锁}LeaveCriticalSection(&pList->critical_sec);
}

三. The Readers/Writers Lock:平衡“读多写少”场景的效率

1. 排他锁定的“效率瓶颈”

之前学的“临界区”“互斥器”都是“排他锁定”——不管是读还是写,同一时间只能有一个线程操作。但实际场景中,“读操作”往往比“写操作”多(比如多人读同一篇文章,很少人修改),此时排他锁定会让所有读线程排队,效率极低。

就像图书馆:如果不管借书(读)还是改书(写),都只允许一个人进,读者会怨声载道。合理的做法是:多个读者可以同时进,写者只能在没人读/写时进

2. 借鉴OpenFile的共享模式

Win16的OpenFile函数有三种共享模式,正好对应这种需求:

  • OF_SHARE_EXCLUSIVE:排他模式(读写都阻止)——对应“写操作”;
  • OF_SHARE_DENY_WRITE:阻止写、允许读——对应“读操作”;
  • OF_SHARE_DENY_READ:阻止读、允许写——对应“写操作”。

Readers/Writers Lock(读写锁)就是实现这种逻辑的工具:

  • 多个读线程可以同时持有“读锁”;
  • 只有一个写线程可以持有“写锁”,且持有期间不允许任何读线程。

3. Readers/Writers Lock的核心算法

读写锁的实现基于“计数器+信号量”,核心逻辑(伪码)如下:

读锁的获取与释放
// 获取读锁
Lock(ReaderMutex); // 保护读计数器,避免多个线程同时改
ReadCount = ReadCount + 1;
if (ReadCount == 1) {Lock(DataSemaphore); // 第一个读者锁定数据,阻止写者
}
Unlock(ReaderMutex);// 释放读锁
Lock(ReaderMutex);
ReadCount = ReadCount - 1;
if (ReadCount == 0) {Unlock(DataSemaphore); // 最后一个读者释放数据,允许写者
}
Unlock(ReaderMutex);
写锁的获取与释放
// 获取写锁:直接锁定数据,阻止所有读/写
Lock(DataSemaphore);// 释放写锁:释放数据,允许读/写
Unlock(DataSemaphore);

4. 读写锁的Win32实现:数据结构与核心函数

步骤1:定义RWLock数据结构

需要三个关键成员:

  • hMutex:保护ReadCount的互斥器(避免多个线程同时修改计数器);
  • hDataLock:控制数据访问的信号量(初始值1,最大值1,类似互斥器);
  • nReaderCount:读线程计数器(记录当前有多少个读线程)。
typedef struct _RWLock {HANDLE hMutex;       // 保护nReaderCount的互斥器HANDLE hDataLock;    // 控制数据访问的信号量int nReaderCount;    // 读线程计数器
} RWLock;
步骤2:初始化读写锁(InitRWLock)
BOOL InitRWLock(RWLock *pLock) {pLock->nReaderCount = 0;// 创建信号量:初始值1,最大值1(允许一个线程锁定)pLock->hDataLock = CreateSemaphore(NULL, 1, 1, NULL);if (pLock->hDataLock == NULL) return FALSE;// 创建互斥器:保护nReaderCountpLock->hMutex = CreateMutex(NULL, FALSE, NULL);if (pLock->hMutex == NULL) {CloseHandle(pLock->hDataLock); // 失败时清理资源return FALSE;}return TRUE;
}
步骤3:获取与释放读锁
// 获取读锁
BOOL AcquireReadLock(RWLock *pLock) {BOOL result = TRUE;// 先锁定互斥器,才能修改nReaderCountif (!MyWaitForSingleObject(pLock->hMutex)) return FALSE;// 第一个读线程需要锁定数据信号量if (++pLock->nReaderCount == 1) {result = MyWaitForSingleObject(pLock->hDataLock);}ReleaseMutex(pLock->hMutex); // 释放互斥器return result;
}// 释放读锁
BOOL ReleaseReadLock(RWLock *pLock) {int result;LONG lPrevCount;if (!MyWaitForSingleObject(pLock->hMutex)) return FALSE;// 最后一个读线程释放数据信号量if (--pLock->nReaderCount == 0) {result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);}ReleaseMutex(pLock->hMutex);return result;
}
步骤4:获取与释放写锁
// 获取写锁:直接锁定数据信号量
BOOL AcquireWriteLock(RWLock *pLock) {return MyWaitForSingleObject(pLock->hDataLock);
}// 释放写锁:释放数据信号量
BOOL ReleaseWriteLock(RWLock *pLock) {int result;LONG lPrevCount;result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);// 检查信号量是否原本被锁定(避免误释放)if (lPrevCount != 0) {FatalError("ReleaseWriteLock - 信号量未被锁定!");}return result;
}

5. 范例程序:READWRIT的作用

文档中的READWRIT程序是读写锁的实战案例:

  • 包含LIST.C(用读写锁保护的链表);
  • 启动4个线程:2个读线程、1个写线程、1个删除线程;
  • 运行后可观察:多个读线程同时读,写线程只有在无读线程时才执行。

四. 我需要锁定吗?:判断锁定的“黄金法则”

很多新手会纠结“什么时候该加锁”,以下是文档给出的核心指导方针(零基础也能直接用):

  1. 不确定就加锁:多线程程序中,“过度锁定”顶多影响效率,“漏加锁”会导致数据错乱,调试难度极高;
  2. 多线程共享数据必加锁:只要多个线程会“使用”同一数据(读、写、比较、修改都算),就必须保护;
  3. 基础类型用Interlocked函数:如果是32位及以下的基础类型(如intDWORD),且只是简单的“加1/减1”,可以用InterlockedIncrement/InterlockedDecrement函数,不用加锁;
  4. 复杂数据用数据库/专用工具:如果数据关联复杂(比如多表关联),可以用支持多线程的数据库(如SQL Server),它自带事务和锁定机制;
  5. 单线程专属数据不加锁:如果数据只在一个线程中创建、修改、销毁,其他线程完全不碰,就不用加锁。

五. Lock Granularity:锁定粒度的“选择艺术”

“锁定粒度”指“一次锁定的数据范围大小”,分两种极端:

  • 粗粒度锁定(Coarse Granularity):一次锁定整个数据结构(比如锁定整个链表);
  • 细粒度锁定(Fine Granularity):一次锁定数据结构的一小部分(比如锁定链表的一个节点)。

1. 两种粒度的对比

特性粗粒度锁定(Coarse Granularity)细粒度锁定(Fine Granularity)
使用难度简单(只需一个锁)复杂(多个锁,易出错)
死锁风险极低(锁少,依赖关系简单)极高(锁多,易形成循环依赖)
效率瓶颈易出现(所有线程等一个锁)较少(锁不冲突时可并行)
锁定开销低(锁的创建/销毁少)高(锁多,切换频繁)

2. 粒度选择建议

  • 新手从粗粒度开始:先锁定整个数据结构,保证程序正确运行;
  • 逐步优化为细粒度:当发现某个锁成为效率瓶颈(比如读线程排队严重),再拆分成多个细粒度锁;
  • 避免过度拆分:比如给10000个CAD形状各加一个锁,会导致代码复杂到无法维护;
  • 提前预防死锁:用细粒度锁时,要规定锁的获取顺序(比如先锁节点A再锁节点B),避免循环等待。

📣 结语

本章从“编译器优化的小坑”(volatile)讲到“多线程数据关联的大坑”(参照完整性),再到“读多写少的效率方案”(读写锁),最后给出“锁定判断和粒度选择”的实战建议——核心逻辑只有一个:多线程数据保护的本质是“平衡正确性和效率”

如果你是零基础,建议先从volatile和“粗粒度锁定”练手,比如用临界区保护一个共享数组;熟练后再尝试读写锁,比如实现一个支持多用户读、单用户写的日志系统。

记得给本文点赞、收藏,后续还会更新Win32多线程的其他章节。跟着实例敲代码,你会发现多线程数据一致性没那么难!

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

相关文章:

  • 大模型在网络安全领域的应用与评测
  • JavaEE--SpringIoC
  • macOS版Sublime简记
  • 机器学习(1)- 机器学习简介
  • 系统架构设计师备考第44天——软件架构演化方式的分类和原则
  • 郑州网站建设公司排行超级工程网站建设上海中心大厦
  • 睢县做网站酒店怎样做网站好评
  • Azure OpenAI 压测和配额规划完整指南
  • Lua C API 中的 lua_rawseti 与 lua_rawgeti 介绍
  • 基于单片机的便携式温湿度检测烘干机设计
  • lua对象池管理工具剖析
  • 网站建设选择什么系统好福建省建设工程执业注册管理中心网站
  • 桐庐建设局网站域名解析入口
  • 数据库flask访问
  • 每日Reddit AI信息汇总 10.17
  • 高可用、高性能、高扩展集群核心区别详解以及高可用介绍
  • 【Linux网络】初识网络,网络的基础概念
  • 手机端网站动效类怎么做seo搜索优化 指数
  • 递归与迭代——力扣101.对称二叉树
  • 中扬立库 × 宁波卡帝亚:小家电之乡的仓储革命,破解制造仓储瓶颈
  • Linux《网络基础》
  • 网络层(IP)
  • 近红外相机在半导体制造领域的应用
  • 网站制作 深圳信科网络公司对比网站
  • 百度旗下所有app列表温州seo排名优化
  • 怎样做让百度收录网站域名设计方案翻译
  • 【whistle】whistle的安装和代理配置
  • 智能油脂润滑系统:降低维护成本与提升生产效率的双赢之道
  • CC17-加油站
  • 【办公类-120-01】20251016 UIBOT下载小说做成docx