
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- 一、理解互斥锁的前提:硬件上下文与线程私有数据
-
- [1.1 CPU 寄存器与硬件上下文](#1.1 CPU 寄存器与硬件上下文)
- [1.2 交换(Swap/Exchange)语义的本质](#1.2 交换(Swap/Exchange)语义的本质)
- 二、基于硬件指令的锁实现原理
-
- [2.1 申请锁(Lock)的完整流程](#2.1 申请锁(Lock)的完整流程)
-
- [2.1.1 步骤一:初始化线程上下文](#2.1.1 步骤一:初始化线程上下文)
- [2.1.2 步骤二:原子交换数据(抢锁的核心)](#2.1.2 步骤二:原子交换数据(抢锁的核心))
- [2.1.3 步骤三:判断与决策](#2.1.3 步骤三:判断与决策)
- [2.1.4 挂起等待与自旋](#2.1.4 挂起等待与自旋)
- [2.2 释放锁(Unlock)的流程](#2.2 释放锁(Unlock)的流程)
- 三、并发场景下的推演分析
-
- [3.1 竞争锁时的线程切换推演](#3.1 竞争锁时的线程切换推演)
- 四、封装互斥锁完整源码
概要&序論
Hello大家好,我是此方, 上一篇我们讲解了互斥量的概念和接口的使用,并补充了一些细节,但是互斥量是如何时间给临界区加锁的呢?本文开始讲解互斥量的底层原理,同时带大家封装一个互斥量。
一、理解互斥锁的前提:硬件上下文与线程私有数据
在探究互斥锁(Mutex)的底层实现机制之前,必须首先厘清 CPU 寄存器与线程上下文之间的关系。
1.1 CPU 寄存器与硬件上下文
在多线程或多进程环境中,CPU 内部的寄存器硬件只有一套 。然而,运行在系统中的线程却可以有成百上千个。
为了实现多线程并发执行,操作系统在进行线程/进程切换时,会将当前线程在 CPU 寄存器中的数据保存到该线程独立的上下文结构中;当该线程再次被调度时,再将这些数据恢复到寄存器中。因此:
- CPU 寄存器硬件只有一套,由所有线程共享使用。
- **CPU 寄存器内的数值(硬件上下文)**属于当前正在执行的线程私有。
1.2 交换(Swap/Exchange)语义的本质
所谓的"锁",本质上是内存中一个表示状态的变量(假设为 1,代表锁可用)。
当使用 swap 或 exchange 指令将内存中的变量值交换到 CPU 寄存器中时,其本质是:将该共享变量的内容,转移到了当前执行流的硬件上下文中。
核心关键点 :
交换(Exchange)不是拷贝(Copy)。由于整个系统中数值
1只有一份,通过硬件级别的原子的交换操作,数值1只可能存在于某一个线程的私有寄存器上下文中。谁成功将1交换到了自己的寄存器中,谁就持有锁。
二、基于硬件指令的锁实现原理
为了保证互斥,体系结构通常提供汇编级别的交换指令(如 x86 的 xchgb 或 swap)。由于该指令在执行时是原子性的(只包含一条硬件指令),它保证了内存与寄存器数据交换过程不可被打断。
assembly
lock:
movb $0, %al ; 给当前线程的 al 寄存器清零
xchgb %al, mutex ; 原子交换 al 寄存器与内存中 mutex 的值
if (al 寄存器的内容 > 0) {
return 0; ; 申请锁成功,进入临界区
} else {
挂起等待 / 自旋; ; 申请锁失败
goto lock;
}
unlock:
movb $1, mutex ; 将内存中的 mutex 重新置为 1
唤醒等待 Mutex 的线程;
return 0;
2.1 申请锁(Lock)的完整流程
2.1.1 步骤一:初始化线程上下文
线程在尝试加锁时,首先执行 movb $0, %al。这一步作用是清理线程自身的硬件上下文,将自己 CPU 寄存器 %al 中的值清零。
2.1.2 步骤二:原子交换数据(抢锁的核心)

随后执行 xchgb %al, mutex,将线程私有寄存器 %al(值为 0)与公共内存中的 mutex(初始值为 1)进行原子交换。
- 线程 A 优先执行 :若内存中的
mutex为 1,交换后,线程 A 的寄存器%al变为 1,而内存中的mutex变为 0。 - 线程 A 独占锁 :此时数值
1正式进入线程 A 的硬件上下文。即便此时发生线程切换,线程 A 也会带着%al = 1的上下文离开 CPU。
2.1.3 步骤三:判断与决策
接下来判断 %al 中的值:
- 加锁成功 :如果
%al > 0(即成功拿到了 1),线程直接返回 0,进入临界区执行代码。 - 加锁失败 :如果后续的线程 B 试图加锁,它执行
xchgb时,内存中的mutex已经被线程 A 改为了 0。因此线程 B 交换得到的%al为 0。由于%al不大于 0,线程 B 申请锁失败。
2.1.4 挂起等待与自旋
申请锁失败的线程会被挂起等待(阻塞)或通过 goto lock 循环反复尝试申请(自旋),直到锁被释放。
2.2 释放锁(Unlock)的流程
当持锁线程离开临界区时,执行释放锁操作:
- 恢复共享状态 :执行
movb $1, mutex,将内存中mutex的值重新置为 1。 - 唤醒等待流:唤醒阻塞在此锁上的其他线程。
- 完成解锁:返回 0,其他线程重新获得竞争锁的机会。
三、并发场景下的推演分析
借助硬件上下文和交换指令,可以有效保证多线程并发时的互斥性。
3.1 竞争锁时的线程切换推演
假设线程 A 在执行 xchgb 指令完成交换后(此时线程 A 的 %al = 1,内存 mutex = 0),突然被系统剥夺 CPU 执行权,发生线程切换:
- 上下文保存 :线程 A 被切走,CPU 将
%al = 1的状态保存到线程 A 的私有上下文数据中。 - 线程 B 尝试抢锁 :线程 B 被调度执行,其寄存器
%al初始化为 0,与内存mutex(此时为 0)进行交换。交换后线程 B 的%al依然是 0,竞争失败,进入挂起或等待状态。 - 线程 A 恢复执行 :线程 A 再次被调度,系统恢复其硬件上下文(
%al = 1)。线程 A 继续执行条件判断if (al > 0),判断成立,成功进入临界区。
整个过程中,由于物理内存中代表锁的 1 始终被独占在某个线程的寄存器上下文中,任何其他线程都无法在未解锁前获取到 1,从而严格保证了临界区同时只有一个线程在执行。
四、封装互斥锁完整源码
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <functional>
#include <vector>
#include <unistd.h>
namespace MyMutex{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock,nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock ;
};
class Mutexgrand
{
public:
Mutexgrand(Mutex& mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~Mutexgrand()
{
_mutex.Unlock();
}
private:
Mutex& _mutex;
};
}
cpp
#include "MyMutex.hpp"
int ticket = 10000;
MyMutex::Mutex mutex;
void *BuyTicket(void *args)
{
std::string *name = (std::string *)args;
std::cout << "I am The Child Thread[" << *name << "]"
<< "My ThreadId is" << pthread_self() << std::endl;
while (true)
{
{
MyMutex::Mutexgrand mutexgrand(mutex);
if (ticket > 0)
{
usleep(1);
ticket--;
std::cout << "进程[" << *name << "]抢到一张票" << "ticket=" << ticket << std::endl;
continue;
}
if(ticket<=0)
{
break;
}
}
}
return nullptr;
}
int main()
{
std::vector<pthread_t> ptd;
for (int i = 0; i < 5; i++)
{
pthread_t tid = 0;
std::string *name = new std::string("Thread" + std::to_string(i));
pthread_create(&tid, NULL, BuyTicket, name);
ptd.push_back(tid);
}
for (int i = 0; i < 5; i++)
{
pthread_join(ptd[i], nullptr);
}
}
好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye! Linux、C++、算法持续连载中,欢迎关注WeChat Official Account 【此方的技术栈】。