今天的目标是:从 OS 视角理解“为什么必须有锁”、“为什么会出现竞态”、“锁为什么能解决”、“CAS 的本质是什么”。
这一层是并发编程最容易混乱的地方,因为它跨越:
OS 调度(Day3)
CPU 缓存一致性(你之前学到部分)
用户态同步原语
Day4 只讲 OS 层和同步原语,不讲 CPU 内存模型(那是 Day5/Day6 的事)。
核心问题:
为什么会有竞态条件(Race Condition)?(OS 层视角)
为什么需要锁?锁解决了什么?
锁的两种实现路径:阻塞锁 vs 自旋锁
为什么需要 CAS?CAS 解决了什么?
今天的关键目标是:从OS的视角,把所有并发错误的根因定位到“共享 × 切换”。
依旧先看第一个问题:
为什么会有竞态?(这篇文章的根本逻辑)
在上一篇 调度器(Scheduler)与线程状态模型 中,我们曾学到:
CPU 随时可能切换线程
多线程共享同一个进程的内存空间
容易得到以下事实:
共享内存 × 不确定时刻切换
= 访问顺序不可控
= 竞态的根因
本质不是“两个线程修改同一变量”
而是:
线程之间的执行交错是不可预测的。
OS 不会告诉你:
会不会切
什么时候切
切到哪条指令之间
所以任何共享可变数据默认就会出现竞态。
这是锁存在的唯一天然理由。
为什么需要锁?锁解决了什么?
锁的出现不是为了解决“多线程修改同一变量”,
而是为了解决:
在临界区(critical section)内禁止调度器切走线程。
一个线程拿到锁后,它可以保证:
在退出锁之前,不会有其他线程进入这段代码
也就是:
锁是一个“对调度器的限制”。
它把某段代码变成原子的:要么执行完,要么没执行。
锁解决的问题是:
两个线程同一时间进入临界区
两个线程交错执行导致状态错乱
读—改—写操作被打断
但是显然还有其他问题,比如:
内存可见性(CPU 层)
缓存传播(后面讲)
语义保证(事务级别)
在这里只讲OS视角。
接下来先看看两类锁:
阻塞锁 vs 自旋锁
调度器是如何配合锁的?
靠阻塞(Block)和自旋(Spin)两种行为。
① 阻塞锁:拿不到锁 → 线程进入 Blocked
流程如下:
Thread A 拿到锁
Thread B 来抢锁
↓
锁不可用 → OS 把 Thread B 扔进 Blocked 状态
↓
Thread A 释放锁 → OS 唤醒 B
特点:
线程让出 CPU(节省 CPU 资源)
唤醒需要调度器参与(慢)
适合持锁时间较长的场景
② 自旋锁:拿不到锁 → 线程疯狂检查锁状态(不让出 CPU)
比如:
while (!lock_available) { // spin }特点:
不进入 Blocked,不让出 CPU
快速检查锁是否可用
若锁很快释放,自旋比阻塞快得多
若锁很慢释放 → 自旋浪费 CPU
CAS 的本质是什么?为什么需要它?
CAS(Compare-And-Swap)的本质是一条CPU 提供的原子指令:
if (*addr == expected) { *addr = new; return success; } else { return fail; }CAS解决的问题不是锁太慢,而是:
需要一种无需进入内核态、不触发调度、不阻塞线程的原子更新方式。
CAS 的特点:
不进入内核态(用户态完成)
不进 Blocked(无调度切换)
无需锁(lock-free)
失败了就重试(loop)
即:
CAS 是构建“无锁算法”的最小原子粒度。
一句话总结,就是:
锁(阻塞、自旋)与 CAS 都是在补偿“共享 × 不确定切换”导致的竞态不可避免性。
继续看五道问题:
Q1:为什么会产生竞态?(一句话)
Q2:锁的本质作用是什么?(一句话)
Q3:阻塞锁与自旋锁的区别是什么?(一句话)
Q4:CAS 的本质是什么?为什么需要它?(一句话)
Q5:为什么有了 CAS 还需要锁?
原思路:
Q1,单论OS层面,因为CPU随时可能切换线程,多线程共享同一个进程的虚拟空间,导致线程之间的执行顺序不可预测
Q2,防止可变共享数据出现竞态
Q3,
Q4,CAS的本质是一条CPU提供的原子指令,为了线程无需进入内核态,不触发调度,不阻塞其他线程
Q5,因为CAS无法根治竞态关系,他只能尽可能的不阻塞其他线程
标准答案:
Q1:线程共享内存且 CPU 可在任意时刻切换执行流,导致执行顺序不可控。
Q2:限制调度,使同一时间只有一个线程能进入临界区。
Q3:阻塞锁失败会让线程进入 Blocked 并让出 CPU;自旋锁失败会忙等而不让出 CPU。
Q4:CAS 是 CPU 提供的原子指令,用于在用户态实现无锁的原子更新
Q5:CAS 只能原子操作单个变量,无法保护复杂临界区。
最终模型:
① 竞态产生于线程共享内存且 CPU 可在任意时刻切换执行流,导致执行顺序不可控。
② 锁的本质作用是限制调度,使同一时间只有一个线程能够进入临界区。
③ 阻塞锁获取失败会让线程进入 Blocked 并让出 CPU;自旋锁获取失败会忙等而不让出 CPU。
④ CAS 是 CPU 提供的原子比较并交换指令,用于在用户态完成无锁的原子更新。
⑤ CAS 只能原子操作单个变量,复杂或长临界区仍必须依赖锁来保证互斥。