WIN32_线程(下)

1. 线程的竞态条件

  • 核心结论: 全局变量 g_Num 并发写操作无同步保护,触发竞态条件(Race Condition / 资源争抢)

    • 预期值: 两个线程各自增 100000 次,g_Num = 200000;
    • 实际输出175664(小于 20w),就是典型多线程数据错乱。
  • 拆解自增 g_Num++ 的汇编本质(关键)

    • g_Num++ 不是 CPU 单条原子指令,拆成三步机器码:

      • MOV EAX, [g_Num]:把全局变量值从内存读到寄存器 EAX
      • INC EAX:寄存器内部 + 1
      • MOV [g_Num], EAX:寄存器写回内存
  • 两个线程在三步中间被 CPU 时间片切换,就会发生:两次自增只生效 1 次。
    举例:

    • 线程 A: 读 g_Num=100 → EAX=100,刚 INC 完还没写回,CPU 切到线程 B
    • 线程 B: 同样读内存 g_Num=100,完成 + 1 写回 → g_Num=101
    • 切回线程 A: 把 EAX=101 写回内存

最终两次 ++,数值只 + 1,永久丢计数。

  • WaitForMultipleObjects (...,TRUE,INFINITE) 的作用

bWaitAll=TRUE:主线程阻塞等待两个子线程全部执行完毕才往下执行 cout,保证输出时机一定在线程跑完之后;

等待逻辑没问题,出错和等待函数无关,只缺线程同步锁。

1.1 解决办法

  • 改动核心: __asm LOCK INC [g_Num]

    • INC [g_Num]:原本拆分为 「读内存→寄存器自增→写回内存」 三步非原子操作,是上一轮数值丢失的根源。
    • LOCK 硬件总线锁前缀: CPU 硬件级锁死系统总线,整条 INC 指令执行期间独占内存地址,其他 CPU 核心 / 线程无法中途抢占读写,把自增变成硬件原子操作。
    • 运行结果: 最终输出严格等于 200000,彻底消除竞态条件。

2. 临界区同步执行

代码定义全局计数变量与临界区对象,主线程初始化临界区后创建两个工作线程,线程循环十万次,每次自增全局变量前通过EnterCriticalSection加锁、自增后LeaveCriticalSection释放锁,保证同一时刻仅单线程修改变量;主线程借助WaitForMultipleObjects阻塞等待两个线程全部执行完毕后打印结果,最终销毁临界区资源,依靠临界区串行保护自增逻辑,规避多线程竞态,输出固定为 200000。


一句话区分

  • 原子 LOCK: 锁内存、不卡线程休眠,仅限单变量单运算

  • 临界区: 锁代码段、抢锁失败线程阻塞休眠,能保护一整段业务逻辑


2.1 问题拓展

如果我们在上锁的情况下,却被主线程强制杀死,那么子线程就处于销毁状态,但是锁依旧在,其余子线程依旧处于阻塞状态,这个问题该当如何?

本代码通过强制 TerminateThread 销毁持有临界区却未释放的线程,造成临界区锁无法回收,后续线程获取锁时永久阻塞死锁,直观验证暴力杀线程极易引发多线程卡死问题。


3. 互斥体所属权限

  • 互斥体,用于防止多线程同时访问或者修改共享资源。

  • 同一时刻只有一个线程可以用于互斥体所有权限,如果一个进程拥有了互斥体的所有权限,则其他请求该互斥体会被阻塞,直到互斥体释放。

    • 创建互斥体: CreateMutex
    • 请求互斥体: WaitForSingleObject
    • 释放互斥体: ReleaseMutex

3.1 通过互斥体办法达到多条线程有序对资源进行修改

  • 程序创建全局互斥量与全局计数变量g_Num,开启两个执行相同逻辑的工作线程;线程内部循环 10 万次,每次先通过互斥量加锁、执行g_Num++自增后释放锁,主线程调用WaitForSingleObject逐个阻塞等待两个子线程全部运行结束,回收句柄后打印最终计数值,依托互斥锁保证全局变量自增的原子性,最终g_Num结果固定为 200000。

3.2 经典错误:主线程获取互斥体,而其余子线程被阻塞

  • CreateMutexW第二个参数改为TRUE ,代表创建者主线程初始拥有该互斥量所有权,两个子线程调用WaitForSingleObject申请锁时全部阻塞挂起,无法执行g_Num++;主线程未主动释放互斥锁就等待子线程结束,形成永久死锁,程序卡死,g_Num始终为 0。

4. 临界区与互斥体区别以及死锁处理

  • 临界区

    • 共享资源的线程同步机制, 临界区在同一进程下的线程之间提供了互斥访问。

    • 每个线程在访问共享资源之前必须先进入临界区,在访问结束在离开的时候必须结束临界区,完成线程同步。

  • 互斥体

    • 线程同步机制,用来限制多线程同时访问共享资源。

    • 互斥体可以同步进程或者线程,且可以跨进程。

  • 性能

    • 临界区在同一进程下的线程中比互斥体更快。
  • 功能

    • 互斥体可以跨进程同步,但临界区只能在同一进程下的线程之间同步。
  • 所有权

    • 互斥体有严格的所有权要求,只有拥有互斥体权限的线程才能够释放它。
  • 死锁

    • 当线程持有锁的情况下意外死亡(异常)

      • 当线程持有临界区锁的情况下意外终结,这个锁不会被释放,导致其他线程被阻塞无法正常执行,造成死锁。

      • 当线程持有互斥体锁的情况下意外终结,Windows会自动释放所有权,其他线程依旧可以正常执行。

5. 死锁权限交接

输出结果:

  • 本代码刻意模拟 Mutex 遗弃特性:线程 1 拿到互斥锁休眠 5 秒后用 TerminateThread强行销毁自身、不手动 ReleaseMutex,Windows 独有规则会把这个互斥标记为遗弃 (Abandoned),系统自动回收锁所有权并唤醒阻塞中的线程 2,线程 2 的 WaitForSingleObject 拿到WAIT_ABANDONED(0x80)返回值、顺利向下执行并打印输出,区别于临界区被强杀永久死锁的表现,同时前期用 GetCurrentThread 拿到固定 - 2 伪句柄,借此区分伪句柄与 GetCurrentThreadId 获取真实 TID 的用法,完整演示互斥体异常遗弃的系统处理逻辑。

6. 内核对象跨进程互斥

  • 本代码利用命名互斥体 Mutex 实现程序单开限制 ,调用 CreateMutex 创建名为L"Win32_Thread"的全局命名互斥对象 ,若程序二次启动,系统检测同名互斥已存在,CreateMutex 依旧返回有效句柄、同时GetLastError返回ERROR_ALREADY_EXISTS ,程序触发弹窗提示 "禁止多开",无论单开还是重复启动最终都会向下执行打印与暂停,程序退出前通过 CloseHandle 释放互斥内核句柄,这套也是 Windows 软件、游戏最经典的进程单实例防多开实现方案。

7. 信号量计数机制以及并发特征

  • 信号量是一种同步对象,用于控制多个线程对共享资源的访问。它是一个计数器,用来表示可用资源的数量。当信号量的值大于0,它表示有资源可用;当值为0,表示没有可用资源。

    • 等待:试图减少信号量的值。如果信号量的值大于0,减1并继续执行。如果信号量的值为0,则线程阻塞,直到信号量的值变为大于0。

    • 释放:增加信号量的值。如果有其他线程因等待这个信号量而阻塞,它们中的一个将被唤醒。

  • 创建信号量

    • 在 Windows 系统中,使用 CreateSemaphore 或 CreateSemaphoreEx 函数创建信号量。
  • 等待(Wait)和释放(Release)信号量

    • 等待信号量通常使用 WaitForSingleObject 或 WaitForMultipleObjects 函数。

    • 释放信号量使用 ReleaseSemaphore 函数。


8. 多线程并发执行

输出结果:

  • 本示例通过初始资源计数与最大上限均为 3 的无名信号量实现并发线程限流控制,主线程批量创建 10 个工作线程,所有子线程启动后率先通过WaitForSingleObject争抢信号量资源,同一时刻仅能放行 3 个线程进入循环打印逻辑,线程循环执行完毕后调用ReleaseSemaphore归还 1 份信号量配额,让阻塞排队的后续线程得以获取资源继续运行,最终主线程借助WaitForMultipleObjects等待全部 10 个子线程执行完毕后程序结束,完整演示了信号量限制最大并发数、实现线程池限流的经典原理。

9. 手动自动重置对象

  • 在Windows编程中,事件是一种同步机制,用于在多个线程之间发送信号。事件对象可以是手动重置或自动重置。

    • 手动重置事件(Manual Reset Event): 当事件被设置(signaled)后,它将保持这个状态直到显式地被重置。这意味着多个等待该事件的线程都可以在事件被重置之前被唤醒。

    • 自动重置事件(Auto Reset Event): 当事件被一个等待的线程接收(signaled)后,系统会自动将事件状态重置为非信号状态(non-signaled)。这意味着每次只允许一个线程被唤醒。

  • 创建事件

    • 使用Windows API函数CreateEvent可以创建一个事件对象

    • lpEventAttributes: 指向安全属性的指针,如果设置为NULL,则使用默认安全性。

    • bManualReset: 如果为TRUE,则创建一个手动重置事件,否则创建自动重置事件。

    • bInitialState: 如果为TRUE,则初始状态为信号状态;如果为FALSE,则为非信号状态。

    • lpName: 事件的名称。

  • 设置事件(将事件状态设置为信号状态)使用SetEvent函数

  • 重置事件(将事件状态设置为非信号状态)使用ResetEvent函数

  • 等待事件 等待一个事件对象变为信号状态使用WaitForSingleObject函数

这段代码创建了手动重置型事件对象 ,初始为无信号状态;随后启动 3 个子线程,所有子线程执行到WaitForSingleObject时全部阻塞等待事件触发。主线程延迟 2 秒后调用SetEvent发出信号,由于是手动事件,所有阻塞线程会被同时唤醒并继续执行。最后主线程通过WaitForMultipleObjects等待全部子线程运行完毕,再依次关闭句柄结束程序,完整演示了利用事件对象批量唤醒线程的同步逻辑。

区别:


10. std::thread

  • 这段代码用 C++11 标准库的 std::thread 创建了一个子线程,让它执行 func 函数,循环打印 10 次 WorkThread!!!;主线程调用 t1.join() 阻塞等待子线程执行完毕后,再继续运行自身的循环,打印 10 次 MainThread!!!,最终控制台输出严格按 "先子线程全部打印、再主线程全部打印" 的顺序执行,清晰演示了线程的创建与 join() 等待同步的基本流程。

11. 线程优先级调度以及时间碎片

  • 这段代码先将当前进程的优先级类设置为HIGH_PRIORITY_CLASS,随后创建一个工作线程,并调用SetThreadPriority将该线程优先级设为THREAD_PRIORITY_HIGHEST;工作线程内部通过GetCurrentThread()获取自身句柄,再用GetThreadPriority读取优先级并打印,最终输出结果为2,对应线程最高优先级;主线程通过WaitForSingleObject等待线程执行完毕,最后关闭句柄,完整演示了进程优先级类与线程优先级的设置、读取和验证流程。

12. 消费者与生产者(单对单)

  • 这段代码实现了一个基于环形缓冲区的经典生产者 - 消费者同步模型,通过 Windows 多线程、互斥体与事件对象,确保线程安全的数据生产与消费流程。程序首先从控制台输入需要生产的产品总数,随后初始化互斥体保证共享内存操作安全,并创建两个自动重置事件:生产者事件初始为有信号状态,确保优先启动生产,消费者事件初始为无信号状态,避免提前执行。

  • 程序创建生产者与消费者两个独立线程,生产者线程负责循环生产数据,先等待生产事件信号,获取互斥体后将数据按In下标存入环形缓冲区,更新下标与产品计数,释放互斥体后唤醒消费者;当缓冲区满时,主动重置生产事件挂起自身。消费者线程持续等待消费事件信号,获取互斥体后按Out下标取出数据并打印,更新下标与产品计数,释放互斥体后立即唤醒生产者;当缓冲区为空时,主动重置消费事件挂起自身。

  • 整个过程依靠互斥体杜绝数据竞争,通过双事件实现线程的有序调度与互相唤醒,严格遵循缓冲区满则生产者等待、缓冲区空则消费者等待的规则,最终完成指定数量产品的稳定生产与消费,待两个线程全部执行完毕后,程序释放所有内核对象并正常退出。


13. 消费者与生产者(多对单)

  • 本次练习基于临界区 + 手动重置事件实现经典生产者消费者模型 ,设置两个生产者线程一个消费者线程,队列最大容量限制为 10。利用手动事件精准管控生产、消费的启停状态:队列未满时置位可生产事件,队列已满则手动重置该事件阻塞生产者;队列有数据时置位可消费事件,队列为空则重置该事件阻塞消费者。所有队列操作、状态判断与事件启停均包裹在临界区内,规避多线程竞态问题,同时增设生产完成标记,保证所有数据消费完毕后消费者正常退出,整套逻辑严格遵循手动事件的使用规则,实现线程同步、队列限流与程序安全收尾。

14. 线程池

方法一:

  • 这段代码演示了 Windows 原生线程池(Thread Pool API) 的基础用法,核心逻辑是创建一个最大线程数为 4、最小线程数为 1 的线程池,通过初始化回调环境、绑定线程池与回调函数,批量提交 20 个异步任务。每个任务在回调函数中打印当前线程 ID 并休眠 500 毫秒,以此直观展示线程池的复用机制 ------20 个任务会被限制在 4 个工作线程内执行,避免频繁创建销毁线程的开销,同时验证了线程池对并发任务的调度与管控能力。

方法二:

  • 这段代码是基于 Windows 原生线程池 API 的基础使用练习,它演示了完整的线程池工作项(TP_WORK)生命周期管理流程:先通过CreateThreadpool创建线程池并设置最大 4 个、最小 1 个工作线程,再用CreateThreadpoolWork创建绑定回调函数的工作项,随后调用SubmitThreadpoolWork提交任务、WaitForThreadpoolWorkCallbacks等待任务执行完毕,最后通过CloseThreadpoolWork释放工作项资源并关闭线程池。回调函数中打印线程 ID 并休眠,直观展示了线程池对任务的调度与复用,完整覆盖了工作项从创建、提交、执行到等待、释放的全流程,体现了线程池资源管理的严谨性。

本章完~

相关推荐
是星辰吖~7 小时前
WIN32_线程(上)
汇编
AI科技星1 天前
数术工坊 · 第四卷 橡皮泥江湖(拓扑学)【完整定稿】
c语言·开发语言·汇编·electron·概率论·拓扑学
iCxhust1 天前
C# 生成命令行程序 将hex格式烧录程序转换成bin烧录格式
开发语言·汇编·单片机·嵌入式硬件·c#·微机原理
iCxhust2 天前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
hhcgchpspk2 天前
汇编语言传递数据和地址的误区
汇编·笔记·nasm·masm
iCxhust2 天前
MTK8088单板机制作(一)时钟电路
汇编·单片机·嵌入式硬件·微机原理·8088单板机
iCxhust2 天前
8086 汇编位测试使用方法
汇编·单片机·嵌入式硬件·微机原理·8088单板机
iCxhust2 天前
用汇编在8088单板机上创建一个进程
汇编·微机原理
AI科技星3 天前
第三卷:质数王朝志(全卷定稿)
c语言·开发语言·汇编·electron·概率论