操作系统

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

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

概况

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

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

步骤 3 / 9

动手:亲手排一次交错,让更新丢掉

计数器从 0 开始,按“T1 读、T2 读、T1 写、T2 写”执行,两次加 1 后结果是 1;加锁后一定是 2。

右边每个线程有三个操作:读(把计数器读进自己的寄存器)、加(寄存器 + 1)、写(写回计数器)。你可以拖动它们决定执行顺序。 计数器初值 0。试这个顺序:T1 读到 0,T2 也读到 0;T1 加 1 写回 1;T2 也加 1 写回 1。两个线程各加了一次,结果却是 1,丢了一次更新。 注意关键不在“同时”,而在 T2 读的时刻 T1 还没写回。只要两个“读”都落在两个“写”之前,就会丢。 再把“使用锁”打开:T1 拿锁后,T2 的读必须等 T1 写完、放锁才能开始,任何顺序最后都是 2。 可以试试:把顺序改成 T1 读、T1 写、T2 读、T2 写,不加锁也是 2。这就是竞态难复现的原因:大多数时候恰好没撞上。

  • 计数器 1
  • T1 寄存器 0 → 1
  • T2 寄存器 0 → 1
事件日志(3)
当前状态发生了什么
两个线程同时运行两个线程读到同一个旧值,更新丢失
交错执行执行顺序不可预测
动手实验两个读都在写之前,丢一次更新