Linux--为什么多线程共享全局变量结果错乱?无锁并发的数据竞争深度剖析
2026/8/10 10:54:16 网站建设 项目流程

为什么多线程共享全局变量结果错乱?并发数据竞争深度剖析

前言

初学多线程时最经典的坑:明明设定100张票,多窗口并发售卖,最后不仅卖出重复票,总售票数还远超100张,变量数值完全不符合预期。这就是数据竞争(竞态条件),根源是多线程无保护并发操作临界资源。本文从基础概念、代码复现、底层汇编原理层层拆解错乱原因,理清原子操作与线程调度逻辑,最后引出解决方案,完整串起互斥锁诞生的背景。

一、四个核心基础概念(通俗释义)

1. 临界资源

多个线程都能读写、共享使用的资源,同一时间只允许一个线程操作,全局变量、全局数组、打开的文件、共享内存都属于临界资源。
售票案例中,全局变量 ticket 就是典型临界资源。

2. 临界区

线程代码里,专门用来访问、修改临界资源的代码片段,也就是操作共享变量的那几行代码。
本例中 if(ticket>0) 判断、打印票数、ticket-- 扣减票数,整段逻辑都属于临界区。

3. 互斥

约束规则:任意时刻,临界区内最多只能有一个线程进入执行;一个线程在临界区操作时,其余线程全部在外等待,以此保护临界资源数据不乱改。

4. 原子性

操作不可拆分、不可被CPU调度打断,只有两种最终状态:完整全部执行完毕,或是完全没开始执行,不存在执行一半暂停的情况。
简单举例:喝水是原子操作,拿起水杯、仰头、放下拆成三步就不是原子操作。

二、问题复现:无锁多线程售票代码

完整源码

#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<pthread.h>// 临界资源:全局共享票数intticket=100;void*route(void*arg){constchar*id=(constchar*)arg;while(1){if(ticket>0){usleep(1000);// 模拟真实售票业务耗时printf("[%s] 售出票:%d\n",id,ticket);ticket--;// 核心问题代码}else{break;}}returnNULL;}intmain(void){pthread_tt[4];constchar*ids[]={"窗口1","窗口2","窗口3","窗口4"};for(inti=0;i<4;i++){pthread_create(&t[i],NULL,route,(void*)ids[i]);}for(inti=0;i<4;i++){pthread_join(t[i],NULL);}return0;}

编译运行指令

gcc test.c-oticket-lpthread./ticket

异常现象

  1. 重复售卖同一张票,多个窗口打印相同票号;
  2. 票数最终出现负数,总售票数量超过初始100张,发生严重超卖;
  3. 每次运行打印顺序、最终票数结果都不一样,结果随机难以复现。

三、逐层拆解:数据错乱两大核心原因

原因1:ticket-- 并非原子操作,底层拆分为三条独立汇编指令

C语言看似一行自减代码,CPU执行时会拆成三步,每一步都可能被线程调度打断:

  1. Load加载:把内存中的 ticket 值读取到CPU寄存器;
  2. Update运算:寄存器内数值减1;
  3. Store写回:将运算后的数值重新写回内存覆盖原值。

举极端场景:初始 ticket=100

  • 线程1:读取100存入寄存器,刚完成加载,操作系统切换CPU,挂起线程1;
  • 线程2:同样读取内存中100,完成三步自减,内存票数变为99;
  • CPU切回线程1,线程1继续执行寄存器里100减1,写回内存,票数再次变成99;
    两次售票操作,票数只减少1,直接造成漏扣票数、后续超卖。

原因2:线程抢占式调度,判断与业务延时存在空隙

代码里 usleep(1000) 模拟售票等待,线程判断 ticket>0 进入代码段后,进入休眠。
此时CPU空闲,操作系统会调度其余线程运行,多个线程先后判断票数有效,全部进入临界区排队等待扣票,休眠结束后统一执行 ticket-- ,同一票号被多个线程卖出。

简单比喻:电影院只剩最后一张票,四个售票员同时查到有票,都收下顾客钱,等出票时才发现票早已被别人卖掉,出现重复出票。

四、竞态条件总结

多个线程并发进入临界区,执行非原子操作,程序最终运行结果依赖操作系统线程调度的先后顺序,这种不确定的竞争现象就叫做竞态条件(Race Condition),也就是我们所说的数据竞争。

局部变量存放在线程私有栈空间,仅当前线程可访问,不存在竞争;只有全局变量、堆内存共享数据这类跨线程可见资源,才会触发数据竞争。

五、数据竞争的解决思路,引出互斥量

想要彻底杜绝票数错乱,必须满足三条约束:

  1. 同一时刻,临界区代码只允许一个线程进入执行;
  2. 临界区空闲时,等待的线程可正常进入,不会无限阻塞;
  3. 线程离开临界区后,及时放行等待线程,互不干扰。

Linux系统提供 **互斥量mutex(互斥锁)**实现以上规则:线程进入临界区前加锁,离开临界区后解锁;持有锁的线程独占临界资源,其余线程加锁时阻塞等待,从根源隔绝多线程同时修改共享变量,消灭数据竞争。

文末小结

  1. 数据竞争本质:共享临界资源 + 非原子操作 + 线程抢占调度;
  2. 单行C代码未必是原子操作,算术自增、判断赋值都可能拆分多条指令;
  3. 全局变量天然存在线程竞争风险,访问时必须划定临界区,通过锁机制实现互斥保护;
  4. 本篇为互斥锁前置理论篇,下一篇将详解pthread_mutex互斥量完整使用规范与编码避坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询