操作系统

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

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

概况

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

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

步骤 8 / 9

动手:用等待图找出死锁

T1 先 A 后 B、T2 先 B 后 A,交错时形成环 T1→B→T2→A→T1,就是死锁;两边都改成先 A 后 B,环消失。

右边设定两把锁 A、B,给每个线程排好加锁顺序,然后一步步执行。图里画“等待图”:线程指向它在等的锁,锁指向拿着它的线程。 试 T1: [A, B],T2: [B, A]。T1 拿到 A,T2 拿到 B;接着 T1 要 B(被 T2 拿着),T2 要 A(被 T1 拿着)。 图里出现一个环:T1 等 B,B 在 T2 手里,T2 等 A,A 在 T1 手里。有环就是死锁,谁也不会放手。 注意它不是每次都发生:如果 T1 一口气拿到 A 和 B 再轮到 T2,就没事。这也是死锁难复现的原因。 再把 T2 改成 [A, B]:两个线程都先抢 A,没抢到的连 B 都不会去碰,环不可能形成。这就是“固定加锁顺序”为什么有效。

事件日志(8)
当前状态发生了什么
两个线程同时运行两个线程读到同一个旧值,更新丢失
交错执行执行顺序不可预测
动手实验两个读都在写之前,丢一次更新
线程 1 持有锁线程 1 独占临界区
线程 1 持有锁线程 2 拿锁失败
线程 2 睡眠 → 被唤醒线程 2 拿到锁,两次加 1 都生效
死锁两个线程都在等
动手实验等待图出现环即死锁