多线程下对锁的访问是如何做到互斥的,普通的变量是无法避免两个线程同时访问的,那么锁是如何避免两个线程同时访问的

发布时间:2026/5/27 22:05:10

多线程下对锁的访问是如何做到互斥的,普通的变量是无法避免两个线程同时访问的,那么锁是如何避免两个线程同时访问的 这是一个非常敏锐的问题你指出了多线程编程中最核心的矛盾如果普通变量会被两个线程同时修改那么用来做锁的变量通常也是一个整数或标志位为什么不会被两个线程同时修改从而失效呢答案的关键在于锁的底层变量确实也是普通内存但访问它的“方式”不同。普通变量的读写是非原子的而锁的底层操作依赖于 CPU 提供的原子指令Atomic Instructions。1. 核心区别原子性 (Atomicity)普通变量的陷阱假设我们用一个普通整数int lock 0;来做锁逻辑是“如果是 0 就改成 1”。代码看似简单if (lock 0) { lock 1; }在机器指令层面这其实是三步走LOAD: 把lock的值从内存读到寄存器比如读到 0。COMPARE: 比较寄存器里的值是不是 0。STORE: 如果是 0把 1 写回内存中的lock。灾难现场线程 A 执行了 LOAD读到 0。此时发生线程切换线程 B 也执行了 LOAD读到 0因为 A 还没写回去。线程 A 继续比较成功执行 STORElock变为 1。线程 A 认为锁获取成功。线程 B 继续比较成功它寄存器里也是 0执行 STORElock变为 1。线程 B 也认为锁获取成功。结果两个线程都进入了临界区锁失效了。锁的解决方案原子指令为了解决这个问题现代 CPUx86, ARM 等提供了一条特殊的指令将“读取、比较、写入”这三个动作合并成一个不可分割的原子动作。在 x86 架构上这条指令通常叫CMPXCHG(Compare and Exchange)配合LOCK前缀。原子操作流程CPU 接收到LOCK CMPXCHG指令。CPU 会锁定内存总线或缓存行Cache Line在这一瞬间其他所有核心都无法访问这个内存地址。CPU 执行读取值 - 比较 - 写入新值。解锁总线/缓存行。在这个过程中没有任何其他线程能插队。如果线程 A 正在执行这条原子指令线程 B 试图访问同一内存地址线程 B 会被硬件强制暂停直到线程 A 的指令完全执行完毕。因此要么 A 成功把 0 变 1B 失败发现值已经是 1要么 B 成功A 失败。绝不可能同时成功。2. 硬件是如何保证的底层机制当你使用std::atomic或底层的锁实现时编译器会生成带有特殊前缀的汇编指令。x86 架构示例普通写入mov [lock], 1 ; 普通写入不保证原子性多核下可能冲突原子写入加锁lock xchg [lock], 1 ; LOCK 前缀 XCHG 指令或者原子比较交换lock cmpxchg [lock], 1LOCK前缀的作用它告诉 CPU“在执行这条指令期间请独占这个内存地址所在的缓存行。”在多核处理器中这通常通过缓存一致性协议如 MESI 协议来实现。当核心 A 想要修改某个变量时它会向总线广播“我要独占这个缓存行”其他核心如核心 B如果也有这个变量的副本必须立即**失效Invalidate**自己的副本。只有当核心 A 拿到独占权后才能修改。在LOCK指令执行完之前核心 A 不会释放独占权核心 B 的任何访问请求都会被硬件挂起等待。这就是硬件层面的“互斥”不是靠软件约定而是靠电路和协议强制串行化。3. 从原子指令到完整的锁仅仅有原子指令还不够因为原子指令只能保证“修改标志位”这一步不出错。完整的锁还需要处理“拿不到锁怎么办”的情况。现代锁如std::mutex的实现逻辑通常是这样的伪代码// 伪代码自旋锁 内核阻塞 的混合实现 void lock() { while (true) { // 1. 尝试用原子指令抢锁 (CAS: Compare-And-Swap) // 期望值是 0 (未锁), 新值是 1 (已锁) // 如果当前值是 0硬件原子地将其改为 1返回 true // 如果当前值是 1什么都不做返回 false if (atomic_compare_exchange_weak(lock_var, 0, 1)) { return; // 抢到了退出函数进入临界区 } // 2. 如果没抢到 (返回 false) // 先自旋一会儿 (Spin)期望对方马上释放避免昂贵的系统调用 for (int i 0; i SPIN_LIMIT; i) { if (atomic_compare_exchange_weak(lock_var, 0, 1)) { return; } // 让出一点点时间片 (pause 指令)减少总线争用 cpu_pause(); } // 3. 如果自旋很久还没抢到说明持有锁的线程可能在干大事 // 调用操作系统接口让自己去睡觉 (阻塞) os_wait_for_lock(lock_var); } } void unlock() { // 原子地将锁变量改回 0 atomic_store(lock_var, 0); // 唤醒一个正在等待的线程 os_wakeup_one_waiter(); }总结普通变量无法互斥因为普通的读 - 改 - 写操作由多条指令组成线程随时可能在中间被切换导致竞态条件。锁能互斥的原因软件层使用了特殊的逻辑循环尝试 阻塞。指令层使用了原子指令如CAS,XCHG。硬件层原子指令利用总线锁或缓存一致性协议 (MESI)强制在多核之间串行化对特定内存地址的访问。结论锁的互斥性不是魔法而是编译器将高级语言映射为 CPU 特有的原子指令并由硬件电路保证这些指令不可被打断的结果。所以虽然锁的状态变量在内存里看起来和普通int没区别但访问它的“钥匙”指令是特殊的这把“钥匙”能命令 CPU 硬件暂时屏蔽其他核心的干扰。

相关新闻