Linux--为什么多线程共享全局变量结果错乱?无锁并发的数据竞争深度剖析
为什么多线程共享全局变量结果错乱并发数据竞争深度剖析前言初学多线程时最经典的坑明明设定100张票多窗口并发售卖最后不仅卖出重复票总售票数还远超100张变量数值完全不符合预期。这就是数据竞争竞态条件根源是多线程无保护并发操作临界资源。本文从基础概念、代码复现、底层汇编原理层层拆解错乱原因理清原子操作与线程调度逻辑最后引出解决方案完整串起互斥锁诞生的背景。一、四个核心基础概念通俗释义1. 临界资源多个线程都能读写、共享使用的资源同一时间只允许一个线程操作全局变量、全局数组、打开的文件、共享内存都属于临界资源。售票案例中全局变量 ticket 就是典型临界资源。2. 临界区线程代码里专门用来访问、修改临界资源的代码片段也就是操作共享变量的那几行代码。本例中 if(ticket0) 判断、打印票数、ticket-- 扣减票数整段逻辑都属于临界区。3. 互斥约束规则任意时刻临界区内最多只能有一个线程进入执行一个线程在临界区操作时其余线程全部在外等待以此保护临界资源数据不乱改。4. 原子性操作不可拆分、不可被CPU调度打断只有两种最终状态完整全部执行完毕或是完全没开始执行不存在执行一半暂停的情况。简单举例喝水是原子操作拿起水杯、仰头、放下拆成三步就不是原子操作。二、问题复现无锁多线程售票代码完整源码#includestdio.h#includestdlib.h#includeunistd.h#includepthread.h// 临界资源全局共享票数intticket100;void*route(void*arg){constchar*id(constchar*)arg;while(1){if(ticket0){usleep(1000);// 模拟真实售票业务耗时printf([%s] 售出票%d\n,id,ticket);ticket--;// 核心问题代码}else{break;}}returnNULL;}intmain(void){pthread_tt[4];constchar*ids[]{窗口1,窗口2,窗口3,窗口4};for(inti0;i4;i){pthread_create(t[i],NULL,route,(void*)ids[i]);}for(inti0;i4;i){pthread_join(t[i],NULL);}return0;}编译运行指令gcc test.c-oticket-lpthread./ticket异常现象重复售卖同一张票多个窗口打印相同票号票数最终出现负数总售票数量超过初始100张发生严重超卖每次运行打印顺序、最终票数结果都不一样结果随机难以复现。三、逐层拆解数据错乱两大核心原因原因1ticket-- 并非原子操作底层拆分为三条独立汇编指令C语言看似一行自减代码CPU执行时会拆成三步每一步都可能被线程调度打断Load加载把内存中的 ticket 值读取到CPU寄存器Update运算寄存器内数值减1Store写回将运算后的数值重新写回内存覆盖原值。举极端场景初始 ticket100线程1读取100存入寄存器刚完成加载操作系统切换CPU挂起线程1线程2同样读取内存中100完成三步自减内存票数变为99CPU切回线程1线程1继续执行寄存器里100减1写回内存票数再次变成99两次售票操作票数只减少1直接造成漏扣票数、后续超卖。原因2线程抢占式调度判断与业务延时存在空隙代码里 usleep(1000) 模拟售票等待线程判断 ticket0 进入代码段后进入休眠。此时CPU空闲操作系统会调度其余线程运行多个线程先后判断票数有效全部进入临界区排队等待扣票休眠结束后统一执行 ticket-- 同一票号被多个线程卖出。简单比喻电影院只剩最后一张票四个售票员同时查到有票都收下顾客钱等出票时才发现票早已被别人卖掉出现重复出票。四、竞态条件总结多个线程并发进入临界区执行非原子操作程序最终运行结果依赖操作系统线程调度的先后顺序这种不确定的竞争现象就叫做竞态条件Race Condition也就是我们所说的数据竞争。局部变量存放在线程私有栈空间仅当前线程可访问不存在竞争只有全局变量、堆内存共享数据这类跨线程可见资源才会触发数据竞争。五、数据竞争的解决思路引出互斥量想要彻底杜绝票数错乱必须满足三条约束同一时刻临界区代码只允许一个线程进入执行临界区空闲时等待的线程可正常进入不会无限阻塞线程离开临界区后及时放行等待线程互不干扰。Linux系统提供 **互斥量mutex互斥锁**实现以上规则线程进入临界区前加锁离开临界区后解锁持有锁的线程独占临界资源其余线程加锁时阻塞等待从根源隔绝多线程同时修改共享变量消灭数据竞争。文末小结数据竞争本质共享临界资源 非原子操作 线程抢占调度单行C代码未必是原子操作算术自增、判断赋值都可能拆分多条指令全局变量天然存在线程竞争风险访问时必须划定临界区通过锁机制实现互斥保护本篇为互斥锁前置理论篇下一篇将详解pthread_mutex互斥量完整使用规范与编码避坑。