在并发程序里,“代码临界区”是指那一小段必须排队使用的共享地带:任何时刻只能有 1 个执行线程进去,否则就像两辆车同时冲进单车道桥——不是撞车就是掉河。临界区的核心意义,就是用各种互斥技术(锁、原子操作等)把这座“桥”管起来,确保里面对共享数据的读改写具有一致性和可预期性。
临界区是什么?
在操作系统教材里,临界区(critical section)是访问共享资源的那段代码;若同时进入者超过 1 个就会出现“竞态条件”——执行顺序不同导致结果错乱。
经典定义强调三个条件:互斥(一次只能进 1 个)、前进(没人抢时不能阻塞自己)、有界等待(永远轮得到其他线程)。
类比 1:单车道桥
想象一座只能让一辆车通行的窄桥:
-
桥面 = 共享资源(内存、文件、寄存器)。
-
上桥的交通灯 = 互斥锁。
-
若南来车已在桥上,北来车必须在灯前等待,直到对向车过桥并放行。
真实工程里,开发者通过 mutex_lock()
或 spin_lock()
把“交通灯”放在临界区入口,确保别的 CPU 不会在你修改链表时闯进来。
类比 2:家里的卫生间
只有一间卫生间:
-
把手锁住→表示“有人”;
-
其他家庭成员在门外忙等(转门把)或排队(坐沙发等通知)——对应自旋锁和互斥锁的差别;
-
出来时开锁并喊“下一个”。
软件里的锁变量就是那把门锁;等待策略不同造就了自旋锁(在原地“捏门把”)和互斥锁(让出 CPU 去干别的)。
技术原理(简化版)
1. 检测&占用
CPU 用一条“读‑改‑写”原子指令尝试把锁从 0 改成 1。成功→进入;失败→等待。
2. 保证互斥
硬件提供 LL/SC 或 CMPXCHG 指令,确保别的核心不可能在中途看到半修改的数据。
3. 退出&唤醒
离开临界区时把锁设回 0,并(若是互斥锁)唤醒下一位等待者。
什么时候一定要用临界区?
场景 | 原因 |
---|---|
引用计数++/-- | 并发更新同一整数,若不互斥计数会丢失。 |
链表/队列头指针修改 | 两线程同时改head 会丢节点或形成环。 |
硬件寄存器读改写 | 多核同时写控制位会触发未知硬件状态。 |
若只是读不写,或操作的是线程私有变量,就不必进入临界区。
选哪种“交通灯”?
锁类型 | 等待方式 | 适合临界区长度 |
---|---|---|
自旋锁 | 原地忙等,CPU 空转 | 极短(µs 级);如中断处理共享计数器。 |
互斥锁 | 沉睡排队,CPU 可做别事 | 较长操作;如磁盘 I/O、内存分配。 |
原子操作 | 无锁,靠硬件一次搞定 | 单变量更新;如atomic_inc() 。 |
若乱用会怎样?
-
死锁:两辆车对向堵桥头,谁也不退;编程里是锁顺序不当。
-
忙等浪费:自旋锁用在长任务会让其他核空转,CPU 利用率暴跌。
-
硬件一致性风暴:锁变量所在缓存行反复失效,系统抖动。
小结
临界区就是程序里的单车道桥/独卫——必须一人一车按序通过。互斥锁、自旋锁、原子操作就是门锁/交通灯的不同实现,负责让线程排队、避免争抢和数据事故。只要记住“共享+修改=要互斥”这条经验,你就能判断什么时候该在代码里划出临界区并加以保护。