Re:Linux系统篇(五十四)线程篇 · 七:互斥锁的底层实现原理:从硬件上下文、原子交换指令(Swap)到并发封装实战


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

  Hello大家好,我是此方, 上一篇我们讲解了互斥量的概念和接口的使用,并补充了一些细节,但是互斥量是如何时间给临界区加锁的呢?本文开始讲解互斥量的底层原理,同时带大家封装一个互斥量

一、理解互斥锁的前提:硬件上下文与线程私有数据

  在探究互斥锁(Mutex)的底层实现机制之前,必须首先厘清 CPU 寄存器与线程上下文之间的关系。

1.1 CPU 寄存器与硬件上下文

  在多线程或多进程环境中,CPU 内部的寄存器硬件只有一套 。然而,运行在系统中的线程却可以有成百上千个。

  为了实现多线程并发执行,操作系统在进行线程/进程切换时,会将当前线程在 CPU 寄存器中的数据保存到该线程独立的上下文结构中;当该线程再次被调度时,再将这些数据恢复到寄存器中。因此:

  • CPU 寄存器硬件只有一套,由所有线程共享使用。
  • **CPU 寄存器内的数值(硬件上下文)**属于当前正在执行的线程私有。

1.2 交换(Swap/Exchange)语义的本质

  所谓的"锁",本质上是内存中一个表示状态的变量(假设为 1,代表锁可用)。

  当使用 swapexchange 指令将内存中的变量值交换到 CPU 寄存器中时,其本质是:将该共享变量的内容,转移到了当前执行流的硬件上下文中

核心关键点

交换(Exchange)不是拷贝(Copy)。由于整个系统中数值 1 只有一份,通过硬件级别的原子的交换操作,数值 1 只可能存在于某一个线程的私有寄存器上下文中。谁成功将 1 交换到了自己的寄存器中,谁就持有锁

二、基于硬件指令的锁实现原理

  为了保证互斥,体系结构通常提供汇编级别的交换指令(如 x86 的 xchgbswap)。由于该指令在执行时是原子性的(只包含一条硬件指令),它保证了内存与寄存器数据交换过程不可被打断。

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 中的值:

  1. 加锁成功 :如果 %al > 0(即成功拿到了 1),线程直接返回 0,进入临界区执行代码。
  2. 加锁失败 :如果后续的线程 B 试图加锁,它执行 xchgb 时,内存中的 mutex 已经被线程 A 改为了 0。因此线程 B 交换得到的 %al 为 0。由于 %al 不大于 0,线程 B 申请锁失败。

2.1.4 挂起等待与自旋

  申请锁失败的线程会被挂起等待(阻塞)或通过 goto lock 循环反复尝试申请(自旋),直到锁被释放。

2.2 释放锁(Unlock)的流程

  当持锁线程离开临界区时,执行释放锁操作:

  1. 恢复共享状态 :执行 movb $1, mutex,将内存中 mutex 的值重新置为 1。
  2. 唤醒等待流:唤醒阻塞在此锁上的其他线程。
  3. 完成解锁:返回 0,其他线程重新获得竞争锁的机会。

三、并发场景下的推演分析

  借助硬件上下文和交换指令,可以有效保证多线程并发时的互斥性。

3.1 竞争锁时的线程切换推演

  假设线程 A 在执行 xchgb 指令完成交换后(此时线程 A 的 %al = 1,内存 mutex = 0),突然被系统剥夺 CPU 执行权,发生线程切换:

  1. 上下文保存 :线程 A 被切走,CPU 将 %al = 1 的状态保存到线程 A 的私有上下文数据中。
  2. 线程 B 尝试抢锁 :线程 B 被调度执行,其寄存器 %al 初始化为 0,与内存 mutex(此时为 0)进行交换。交换后线程 B 的 %al 依然是 0,竞争失败,进入挂起或等待状态。
  3. 线程 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 【此方的技术栈】。

相关推荐
郭涤生1 小时前
rootfs 详解与裁剪优化记录
linux·c++·bsp
二宝哥2 小时前
CentOS 7.9 离线安装 Nginx 并配置openssl1.1.1
linux·nginx·centos
程序员老陆2 小时前
说一说FFmpeg6在Linux平台的编译
linux·运维·服务器·ffmpeg
玫幽倩3 小时前
2026黄河流域公安院校-电子物证单项赛(程序逆向分析+服务器取证)
运维·服务器·python·电子取证·逆向·程序分析·服务器取证
IT摆渡者3 小时前
VM EXSI7.0存储硬盘故障处理
大数据·linux·github
赵民勇3 小时前
systemd-cgtop命令详解
linux·运维
Zhu7584 小时前
在docker环境部署frp
运维·docker·容器
hhwyqwqhhwy4 小时前
Ubuntu22.04 安装vitis
linux
运维行者_4 小时前
企业带宽监控工具实战:网络流量分析与异常排查的5个关键能力
运维·服务器·开发语言·网络·分布式·后端·php