操作系统

两个线程抢一把锁:竞态与互斥

多个线程同时改同一份数据会出错,锁保证同一时刻只有一个线程在改。

概况

以 Linux 上两个线程同时给计数器加 1、用 pthread_mutex 保护为例。

一句话总结
共享数据要加锁,锁靠原子指令实现,抢不到时靠内核让线程睡眠
竞态
结果取决于谁先谁后,偶尔出错,很难复现
临界区
访问共享数据的那段代码,同一时刻只允许一个线程进
Linux 的锁
pthread_mutex 基于 futex:没人抢时不进内核,抢不到才请内核让自己睡
代价
锁让线程排队,锁住的范围越小越好
死锁
四个条件同时成立才会发生;固定加锁顺序是最常用的预防
自旋锁
等待时间极短时空转比睡眠更划算,内核里常用
其他同步
条件变量、读写锁、信号量、原子变量

步骤 5 / 9

锁本身怎么做:需要原子指令

“看锁空不空,空就拿走”必须一步完成,这要靠 CPU 的原子指令。

如果先读“锁是空的”,再写“锁归我”,两个线程可能同时看到空,都以为自己拿到了。 所以 CPU 提供原子指令(x86-64 上如 lock cmpxchg),把“检查并设置”合成一步,别的核心插不进来。 没人抢锁时,加锁只是一条原子指令,完全不用进内核,很快。

线程
共享数据
内核
线程 1
线程 2
共享计数器 / 锁
内核
原子地尝试拿锁:失败
事件日志(5)
当前状态发生了什么
两个线程同时运行两个线程读到同一个旧值,更新丢失
交错执行执行顺序不可预测
动手实验两个读都在写之前,丢一次更新
线程 1 持有锁线程 1 独占临界区
线程 1 持有锁线程 2 拿锁失败