Linux 线程同步与互斥(一):彻底搞懂线程互斥原理、互斥量底层实现与 RAII 封装


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 线程互斥核心概念:先搞懂这 4 个名词,互斥就有了简单的概念雏形](#一. 线程互斥核心概念:先搞懂这 4 个名词,互斥就有了简单的概念雏形)
    • [1.1 共享资源](#1.1 共享资源)
    • [1.2 临界资源](#1.2 临界资源)
    • [1.3 临界区](#1.3 临界区)
    • [1.4 互斥与原子性](#1.4 互斥与原子性)
  • [二. 为什么多线程操作共享资源会出问题?售票系统案例深度拆解](#二. 为什么多线程操作共享资源会出问题?售票系统案例深度拆解)
    • [2.1 有问题的售票代码](#2.1 有问题的售票代码)
    • [2.2 异常运行结果](#2.2 异常运行结果)
    • [2.3 问题根源深度分析](#2.3 问题根源深度分析)
  • [三. 互斥量 mutex:解决临界资源并发访问的核心方案](#三. 互斥量 mutex:解决临界资源并发访问的核心方案)
    • [3.1 互斥量的核心接口](#3.1 互斥量的核心接口)
      • [3.1.1 初始化互斥量](#3.1.1 初始化互斥量)
      • [3.1.2 销毁互斥量](#3.1.2 销毁互斥量)
      • [3.1.3 加锁与解锁](#3.1.3 加锁与解锁)
    • [3.2 用互斥量修复售票系统(全局 && 局部,可以线看上面的图示再来理解)](#3.2 用互斥量修复售票系统(全局 && 局部,可以线看上面的图示再来理解))
  • [四. 互斥量的底层实现原理:为什么加锁能保证原子性?](#四. 互斥量的底层实现原理:为什么加锁能保证原子性?)
    • [4.1 核心硬件支持:swap/exchange 指令](#4.1 核心硬件支持:swap/exchange 指令)
    • [4.2 加锁与解锁的伪代码实现(可以先理解上面这个图示)](#4.2 加锁与解锁的伪代码实现(可以先理解上面这个图示))
  • [五. 互斥量的 C++ 封装:RAII 风格的锁管理](#五. 互斥量的 C++ 封装:RAII 风格的锁管理)
    • [5.1 互斥量 Mutex 类封装](#5.1 互斥量 Mutex 类封装)
    • [5.2 RAII 风格的锁守卫 LockGuard](#5.2 RAII 风格的锁守卫 LockGuard)
    • [5.3 用封装后的锁重构售票代码](#5.3 用封装后的锁重构售票代码)
  • [六. 全文总结与面试高频考点](#六. 全文总结与面试高频考点)
  • 结尾:

前言:

大家好,我是深耕 Linux 后端开发与系统编程的 CSDN 博主。在多线程编程中,线程互斥 是我们解决并发安全问题的第一把钥匙,也是后端开发面试的必考点。但很多同学对互斥的理解,只停留在pthread_mutex_lockpthread_mutex_unlock的 API 调用上,不仅写的代码频繁出现超卖、数据错乱等线程安全问题,面试时被问到临界资源、原子性、互斥量底层实现也一知半解。本文从基础概念、问题场景、互斥量使用,到底层实现原理、C++ 面向对象封装,全流程拆解 Linux 线程互斥,所有代码均可直接复制运行,看完就能彻底吃透线程互斥的核心逻辑。


一. 线程互斥核心概念:先搞懂这 4 个名词,互斥就有了简单的概念雏形

在正式讲互斥实现之前,我们必须先把几个核心概念掰扯清楚,这是理解所有线程安全问题的基础,是需要重点强调的内容。

1.1 共享资源

同一个进程的多个线程,天然共享进程的绝大部分资源,这些能被多个线程同时访问到的资源,就叫做共享资源

比如我们最常见的:

  • 全局变量、静态变量(进程地址空间的数据段,所有线程可见)
  • 进程打开的文件描述符表(一个线程打开的文件,其他线程可直接读写)
  • 堆内存(一个线程 malloc 的空间,其他线程可直接访问)
  • 显示器 / 终端(我们多个线程同时调用printf/cout打印,本质就是同时访问终端这个共享资源)

多线程的绝大多数安全问题,都源于并发访问共享资源,而互斥,就是为了解决这个问题而生的。

1.2 临界资源

多线程执行流中,被保护起来的共享资源,就叫做临界资源。简单来说,我们不希望多个线程同时修改、甚至同时读取的共享资源,就是需要保护的临界资源。比如电商系统的库存、售票系统的余票、多线程共享的计数器,都是典型的临界资源。

1.3 临界区

每个线程内部,访问临界资源的代码,就叫做临界区。我们对临界资源的保护,本质就是对临界区代码的保护。互斥的核心作用,就是保证同一时间,只有一个线程能进入临界区执行代码,从而避免多个线程同时操作临界资源。

1.4 互斥与原子性

  • 互斥:任何时刻,保证有且只有一个执行流进入临界区、访问临界资源,对临界资源起到排他性的保护作用。
  • 原子性:不会被任何操作系统调度机制打断的操作,该操作只有两种状态:要么完全执行完成,要么完全没执行,不存在执行到一半被切换走的情况。

我们后面要讲的互斥锁,本质就是通过原子操作,实现了临界区代码的互斥执行。




二. 为什么多线程操作共享资源会出问题?售票系统案例深度拆解

光讲概念太抽象,我们直接用一个经典的多线程售票系统案例,看看无保护的并发访问,到底会引发什么问题,以及问题的根源是什么。下面的案例有部分是直接使用的上篇线程博客中封装好的线程的代码的

2.1 有问题的售票代码

  • Thread.hpp
cpp 复制代码
#ifndef __THREAD_HPP
#define __THREAD_HPP

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> /* Definition of SYS_* constants */

using func_t = std::function<void()>;

enum class TSTATUS
{
    THREAD_NEW,
    THREAD_RUNNING,
    THREAD_STOP
};

// bug
static int gnum = 1;

class Thread
{
private:
    void getprocessid()
    {
        _pid = getpid();
    }
    void getlwp()
    {
        _lwpid = syscall(SYS_gettid);
    }
    static void *routine(void *args)
    {
        Thread *ts = static_cast<Thread *>(args);
        ts->getprocessid();
        ts->getlwp();
        pthread_setname_np(pthread_self(), ts->Name().c_str());
        ts->_func();

        return nullptr;
    }

public:
    Thread(func_t f) : _joinable(true), _status(TSTATUS::THREAD_NEW),_func(f)
    {
        _name = "thread-" + std::to_string(gnum++);
    }
    void start()
    {
        if (_status == TSTATUS::THREAD_RUNNING)
        {
            std::cout << "thread is already running" << std::endl;
            return;
        }
        int n = pthread_create(&_tid, nullptr, routine, this);
        (void)n;
        _status = TSTATUS::THREAD_RUNNING;
    }
    void stop()
    {
        if (_status == TSTATUS::THREAD_RUNNING)
        {
            int n = pthread_cancel(_tid);
            (void)n;
            _status = TSTATUS::THREAD_STOP;
        }
        else
        {
            std::cout << "thread status is : THREAD_NEW or THREAD_STOP! stop error" << std::endl;
        }
    }
    void join()
    {
        if (_joinable)
        {
            int n = pthread_join(_tid, nullptr);
            (void)n;
            printf("lwp : %d, name: %s, join success\n", _lwpid, _name.c_str());
        }
        else{
            printf("lwp : %d, name: %s, join failed, because thread is detach\n", _lwpid, _name.c_str());
        }
    }
    void detach()
    {
        if (_joinable && _status == TSTATUS::THREAD_RUNNING)
        {
            _joinable = false;
            int n = pthread_detach(_tid);
            (void)n;
        }
    }
    std::string Name()
    {
        return _name;
    }
    ~Thread()
    {
    }

private:
    pthread_t _tid;
    pid_t _pid;
    pid_t _lwpid;
    std::string _name;
    bool _joinable;
    TSTATUS _status;
    func_t _func;
};

#endif
  • main.cc
cpp 复制代码
#include <iostream>
#include <vector>
#include <chrono>
#include <cstdint>
#include "Thread.hpp"
using namespace std;

int tickects = 5000;

void route()
{
    char name[64];
    pthread_getname_np(pthread_self(), name, sizeof(name));

    while(1)
    {
        if(tickects > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, tickects);
            tickects--;
        }
        else {
            break;
        }
    }
    std::cout << name << std::endl;
}
int main()
{
    vector<Thread> threads;
    for(int i = 0; i < 4; i++)
    {
        threads.emplace_back(route);
    }

    for(auto& thread: threads)
    {
        thread.start();
    }

    for(auto& thread : threads)
    {
        thread.join();
    }
    
    return 0;
}

2.2 异常运行结果

  • 我们运行之后结果如下,怎么编译运行的这里就不展示了,可以写个Makefile
cpp 复制代码
thread 4 sells ticket:100
thread 1 sells ticket:100
thread 3 sells ticket:100
thread 2 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

我们发现了两个严重的问题

  • 同一张票被多个线程重复售卖:100 号票被 4 个线程同时抢到;
  • 出现超卖现象:余票卖到了 0、-1、-2,完全不符合业务逻辑。


2.3 问题根源深度分析

为什么会出现这种情况?核心原因有 3 个,每一个都直击多线程并发的本质:

  • if 判断的并发穿透

ticket=1时,线程 1 执行if(ticket>0)判断为真,刚要进入代码块,操作系统发生线程调度,把 CPU 切换给了线程 2、3、4。此时ticket的值还没被修改,其他 3 个线程的if判断也都为真,全部进入了代码块,最终 4 个线程都会执行ticket--,自然就出现了 0 和负数票。

  • 耗时窗口导致的并发进入

代码中的usleep(1000)模拟了真实业务的耗时,在这 1ms 的时间里,操作系统可以完成数十次线程切换,大量线程都会在这个窗口里进入临界区,同时操作ticket变量。

  • ticket--不是原子操作

我们写的一行 C 语言代码ticket--,在 CPU 执行时,会被拆解成3 条汇编指令

cpp 复制代码
; 1. load:把共享变量ticket从内存加载到CPU寄存器中
mov  0x2004e3(%rip),%eax
; 2. update:更新寄存器里的值,执行-1操作
sub  $0x1,%eax
; 3. store:把新值从寄存器写回内存中的ticket变量
mov  %eax,0x2004da(%rip)

这 3 条指令不是原子的,执行过程中随时可能被操作系统打断,发生线程切换。我们举个最典型的场景:

  • ticket=1时,线程 1 执行完 load 和 update,寄存器里的值已经变成 0,但还没执行 store 写回内存,此时被切换走;
  • 线程 2 被调度执行,此时内存里的ticket还是 1,if判断为真,完整执行了 3 条指令,把ticket改成了 0;
  • 线程 1 被切回来,继续执行 store,把寄存器里的 0 写回内存,ticket还是 0,相当于多执行了一次扣减;
  • 如果有更多线程进入,最终就会出现负数票的情况。
  • 认知补充:只要形成的汇编语句不止一条,这个操作就不是"原子的",随时可能被打断!

要解决这个问题,核心就是要保证:同一时间,只有一个线程能进入临界区,执行ticket的判断和扣减操作,这就是互斥量要做的事情。


三. 互斥量 mutex:解决临界资源并发访问的核心方案

Linux 上提供的互斥锁(互斥量 mutex),就是专门用来实现线程互斥的工具。它的核心逻辑很简单:

  • 进入临界区之前,必须先申请加锁;
  • 只有成功拿到锁的线程,才能进入临界区执行代码;
  • 临界区代码执行完毕后,必须释放锁,让其他线程可以竞争锁;
  • 如果锁已经被其他线程持有,当前申请锁的线程会被阻塞挂起,直到锁被释放。


3.1 互斥量的核心接口

POSIX 线程库中,互斥量的操作接口都在<pthread.h>头文件中,编译时必须链接-lpthread库(看系统版本之前说过)。

3.1.1 初始化互斥量

初始化互斥量有两种方式:静态初始化和动态初始化。

① 静态初始化

适用于全局 / 静态的互斥量,直接用宏赋值即可,无需手动销毁:

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

② 动态初始化

适用于局部互斥量、需要自定义属性的互斥量,函数原型:

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrict attr);
  • 参数mutex:要初始化的互斥量指针;
  • 参数attr:互斥量属性,填 NULL 表示使用默认属性;
  • 返回值:成功返回 0,失败返回错误码。

3.1.2 销毁互斥量

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁注意事项

  • PTHREAD_MUTEX_INITIALIZER静态初始化的互斥量,不需要销毁;
  • 不要销毁一个已经加锁的互斥量;
  • 已经销毁的互斥量,要确保后续不会有线程再尝试加锁。

3.1.3 加锁与解锁

cpp 复制代码
// 加锁:锁被占用则阻塞等待
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁:释放持有的锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回 0,失败返回错误码。

加锁的两种情况

  • 互斥量处于未锁状态:函数会将互斥量锁定,同时立即返回成功;
  • 互斥量已经被其他线程锁定:调用pthread_mutex_lock的线程会被阻塞挂起,直到互斥量被解锁,被唤醒后重新竞争锁。

3.2 用互斥量修复售票系统(全局 && 局部,可以线看上面的图示再来理解)

我们用互斥量对上面的售票代码进行改造,核心就是给临界区完整加锁

  • 全局(我们还是用的自己封装的,这里就不再展示一次了,大家还可以自己注释掉加锁解锁对比一下时间效率啥的)
cpp 复制代码
// 全局的锁
#include <iostream>
#include <vector>
#include <chrono>
#include <cstdint>
#include "Thread.hpp"
using namespace std;

int tickects = 5000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

// 返回从1970-01-01 00:00:00 UTC 开始的微秒数
uint64_t getMicrosecondTimestamp() {
    auto now = std::chrono::system_clock::now();
    auto microseconds = std::chrono::time_point_cast<std::chrono::microseconds>(now);
    auto epoch = microseconds.time_since_epoch();
    return static_cast<uint64_t>(epoch.count());
}

void route()
{
    char name[64];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    uint64_t start = getMicrosecondTimestamp();

    while(1)
    {
        // 加锁
        pthread_mutex_lock(&glock);
        if(tickects > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, tickects);
            tickects--;
            // 解锁
            pthread_mutex_unlock(&glock);
        }
        else {
            pthread_mutex_unlock(&glock);
            break;
        }
    }
    uint64_t end = getMicrosecondTimestamp();
    std::cout << name << " cast: " << end - start << std::endl;
}
int main()
{
    vector<Thread> threads;
    for(int i = 0; i < 4; i++)
    {
        threads.emplace_back(route);
    }

    for(auto& thread: threads)
    {
        thread.start();
    }

    for(auto& thread : threads)
    {
        thread.join();
    }
    
    return 0;
}
  • 局部(用线程库的)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>
#include <string>

int tickects = 1000;

typedef struct threadData
{
    std::string name;
    pthread_mutex_t* plock;
}threaddata_t;

void* route(void* arg)
{
    threaddata_t* td = static_cast<threaddata_t*>(arg);
    while(1)
    {
        // 加锁
        pthread_mutex_lock(td->plock);
        if(tickects > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", td->name.c_str(), tickects);
            tickects--;
            pthread_mutex_unlock(td->plock);
        }
        else {
            pthread_mutex_unlock(td->plock);
            break;
        }
    }
    
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;
    
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);

    threaddata_t data1 = {"thread1", &lock};
    threaddata_t data2 = {"thread2", &lock};
    threaddata_t data3 = {"thread3", &lock};
    threaddata_t data4 = {"thread4", &lock};

    pthread_create(&t1, nullptr, route, &data1);
    pthread_create(&t2, nullptr, route, &data2);
    pthread_create(&t3, nullptr, route, &data3);
    pthread_create(&t4, nullptr, route, &data4);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    pthread_mutex_destroy(&lock);

    return 0;
}

再次编译运行,会发现不会再出现重复售票和超卖的问题,所有票号按顺序售卖,余票到 1 之后就正常退出,完全符合预期。
加锁的核心注意点

  • 加锁的粒度要尽可能小:只给临界区代码加锁,非临界区代码不要加锁,否则会导致多线程完全串行执行,失去并发的意义;
  • 所有临界区出口都必须解锁:包括正常执行完、break、return、goto 等所有退出路径,否则会导致锁永远不释放,其他线程永久阻塞,造成死锁;
  • 不能重复加锁:同一个线程重复调用pthread_mutex_lock,会导致自己阻塞自己,造成死锁。

四. 互斥量的底层实现原理:为什么加锁能保证原子性?

❓️很多人会问:加锁操作本身是不是原子的?如果两个线程同时调用pthread_mutex_lock,会不会出现两个线程同时拿到锁的情况?

✅️ 答案是:不会。互斥量的加锁操作,本质是通过 CPU 提供的原子交换指令实现的,从硬件层面保证了操作的原子性。

  • 但是这种不是重点,我们继续往下看吧!

4.1 核心硬件支持:swap/exchange 指令

绝大多数 CPU 体系结构,都提供了swapexchange指令,它的核心作用是:把寄存器和内存单元的数据做原子交换

这条指令的关键特性

  • 它是单条 CPU 指令,执行过程中不会被操作系统的调度机制打断;
  • 即使是多处理器多核平台,CPU 访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令必须等待总线周期结束,保证了多核下的原子性。


4.2 加锁与解锁的伪代码实现(可以先理解上面这个图示)

我们结合上面图里的伪代码,拆解 lock 和 unlock 的底层逻辑,就能彻底搞懂互斥量的实现原理。

① 加锁 lock 伪代码

cpp 复制代码
lock:
    movb $0, %al          ; 把al寄存器的值置为0
    xchgb %al, mutex      ; 原子交换:al寄存器和内存中的mutex变量交换值
    if(al寄存器的内容 > 0){
        ; 交换前mutex的值是1(未锁),交换后al=1,mutex=0,加锁成功
        return 0;
    } else {
        ; 交换前mutex的值是0(已锁),加锁失败,挂起等待
        挂起等待;
        goto lock;  ; 被唤醒后,重新尝试加锁
    }

加锁逻辑解读

  • 我们约定:mutex=1表示锁未被持有,mutex=0表示锁已被持有;
  • 线程先把 al 寄存器置为 0,然后执行原子交换指令,把寄存器的值和内存中的 mutex 值做交换;
  • 如果交换后 al 寄存器的值是 1,说明之前 mutex 是 1(锁空闲),现在 mutex 已经被改成 0,加锁成功;
  • 如果交换后 al 寄存器的值是 0,说明之前 mutex 已经是 0(锁被占用),加锁失败,线程被阻塞挂起,等待锁释放后被唤醒,重新尝试加锁。

② 解锁 unlock 伪代码

cpp 复制代码
unlock:
    movb $1, mutex        ; 把mutex的值置为1,释放锁
    唤醒等待mutex的线程;   ; 唤醒所有阻塞等待这把锁的线程
    return 0;

解锁逻辑解读

  • 把内存中的 mutex 值重新置为 1,释放锁;
  • 唤醒所有阻塞等待这把锁的线程,让它们重新竞争锁。

这就是互斥量的核心底层逻辑:通过 CPU 的原子交换指令,保证了 "判断锁状态 + 修改锁状态" 的操作是原子的,不会出现两个线程同时拿到锁的情况。


五. 互斥量的 C++ 封装:RAII 风格的锁管理

在 C++ 开发中,直接使用原生的pthread_mutex_*接口,很容易出现忘记解锁、异常退出未解锁等问题,最终导致死锁。参考文档中给我们提供了非常优雅的解决方案:基于 RAII 机制封装互斥量

RAII 是 C++ 的核心编程思想:利用对象的生命周期来管理资源,在对象构造时完成资源的初始化,在对象析构时完成资源的释放,保证资源在任何情况下都能被正确释放。

5.1 互斥量 Mutex 类封装

我们先封装一个基础的 Mutex 类,屏蔽原生 C 接口的细节,提供简洁的加锁、解锁接口:

cpp 复制代码
// Mutex.hpp
#ifndef MUTEX_HPP
#define MUTEX_HPP

#include <iostream>
#include <mutex>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(&_lock);
    }
private:
    pthread_mutex_t _lock;
};
#endif
  • 另一个版本(可以选择的看看,其实大致一样)
cpp 复制代码
// Lock.hpp
#pragma once
#include <iostream>
#include <pthread.h>

namespace LockModule
{
    // 互斥量封装类
    class Mutex
    {
    public:
        // 禁用拷贝构造和赋值运算符:锁是不可拷贝的资源
        Mutex(const Mutex &) = delete;
        const Mutex &operator=(const Mutex &) = delete;

        // 构造函数:初始化互斥量
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }

        // 加锁接口
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }

        // 解锁接口
        void Unlock()
        {
            pthread_mutex_unlock(&_mutex);
        }

        // 获取原生互斥量指针,用于和条件变量等原生接口配合
        pthread_mutex_t *GetMutexOriginal()
        {
            return &_mutex;
        }

        // 析构函数:销毁互斥量
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex; // 原生互斥量
    };
}

代码解读

  • 禁用拷贝构造和赋值:互斥锁是独占的系统资源,不支持拷贝和赋值,避免出现重复销毁、锁状态错乱的问题;
  • 构造函数完成互斥量的初始化,析构函数完成互斥量的销毁,符合 RAII 思想;
  • 封装了 Lock/Unlock 接口,屏蔽了原生 C 接口的细节,使用更简洁;
  • 提供了获取原生互斥量指针的接口,方便后续和条件变量等原生 POSIX 接口配合使用。

5.2 RAII 风格的锁守卫 LockGuard

有了 Mutex 类,我们再封装一个 LockGuard 类(追加在下面实现即可),实现构造时自动加锁,析构时自动解锁,彻底解决忘记解锁的问题:

cpp 复制代码
// Mutex.hpp 追加内容
#ifndef MUTEX_HPP
#define MUTEX_HPP
class LockGuard
{
public:
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        _lockptr->Lock();
    }
    ~LockGuard()
    {
        _lockptr->UnLock();
    }
private:
    Mutex* _lockptr;
};
#endif
  • 另一个版本(可以选择的看看,其实大致一样)
cpp 复制代码
// Lock.hpp 追加内容
namespace LockModule
{
    // 省略上面的Mutex类...

    // RAII风格的锁守卫
    class LockGuard
    {
    public:
        // 构造函数:传入互斥量,自动加锁
        LockGuard(Mutex &mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }

        // 析构函数:对象生命周期结束时,自动解锁
        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        Mutex &_mutex; // 引用互斥量对象,避免拷贝
    };
}

代码解读

  • 构造函数接收一个 Mutex 对象的引用,调用 Lock () 自动加锁;
  • 析构函数调用 Unlock () 自动解锁,无论函数是正常 return,还是异常退出,只要 LockGuard 对象离开作用域,就会自动调用析构函数解锁,绝对不会出现漏解锁的情况;
  • 这和 C++11 标准库中的std::lock_guard<std::mutex>是完全相同的设计思想。

5.3 用封装后的锁重构售票代码

我们用上面封装的 LockGuard,重构售票系统代码,代码会变得更简洁、更安全:

cpp 复制代码
// #include <iostream>
// #include <thread>
// #include <unistd.h>
// #include <mutex>
// #include <string>
// #include "Mutex.hpp"

// int tickets = 1000;
// Mutex lock;
// // std::mutex mutex;   

// void grabTicket(const std::string& name) 
// {
//     while (true) 
//     {
//         lock.Lock();
//         // mutex.lock();
//         if (tickets > 0) 
//         {
//             usleep(1000);
//             std::cout << name << " grabbed ticket, remaining: " << --tickets << std::endl;
//             // mutex.unlock();
//             lock.UnLock();
//         } else 
//         {
//             // mutex.unlock();
//             lock.UnLock();
//             break;
//         }
//     }
// }

// int main() 
// {
//     const int THREAD_COUNT = 4;
//     std::thread threads[THREAD_COUNT]; // 创建一个数组去存线线程
    
//     // 创建线程
//     for (int i = 0; i < THREAD_COUNT; i++) 
//     {
//         std::string name = "Thread-" + std::to_string(i + 1);
//         threads[i] = std::thread(grabTicket, name);
//     }

//     // 等待所有线程结束
//     for (int i = 0; i < THREAD_COUNT; i++) 
//     {
//         threads[i].join();
//     }
//     return 0;
// }


// RAII模式
#include <iostream>
#include <thread>
#include <unistd.h>
#include <mutex>
#include <string>
#include "Mutex.hpp"

int tickets = 1000;
Mutex lock;
// std::mutex mutex;   

void grabTicket(const std::string& name) 
{
    while (true) 
    {
        LockGuard lockGuard(&lock);
        // std::lock_guard<std::mutex> lockGuard(mutex);
        if (tickets > 0) 
        {
            usleep(1000);
            std::cout << name << " grabbed ticket, remaining: " << --tickets << std::endl;
        } else 
        {
            break;
        }
    }
}

int main() 
{
    const int THREAD_COUNT = 4;
    std::thread threads[THREAD_COUNT]; // 创建一个数组去存线线程
    
    // 创建线程
    for (int i = 0; i < THREAD_COUNT; i++) 
    {
        std::string name = "Thread-" + std::to_string(i + 1);
        threads[i] = std::thread(grabTicket, name);
    }

    // 等待所有线程结束
    for (int i = 0; i < THREAD_COUNT; i++) 
    {
        threads[i].join();
    }
    return 0;
}
  • 另一个版本(可以选择的看看,其实大致一样)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"

using namespace LockModule;

// 全局共享的临界资源:100张余票
int ticket = 1000;
// 定义封装后的互斥量
Mutex mutex;

void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        // 定义LockGuard对象:构造时自动加锁
        LockGuard lockguard(mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
        // LockGuard对象离开作用域,析构时自动解锁
    }
    return nullptr;
}

int main(void)
{
    pthread_t t1, t2, t3, t4;

    // 创建4个抢票线程
    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

可以看到,重构后的代码不需要我们手动调用 unlock,完全不用担心漏解锁导致的死锁问题,代码更简洁,安全性也大大提升。


六. 全文总结与面试高频考点

本文从基础概念到问题场景,从底层原理到工程封装,完整拆解了 Linux 线程互斥的全部核心内容,这里给大家总结面试的一些核心考点:

  • 核心概念:临界资源是被保护的共享资源,临界区是访问临界资源的代码,互斥保证同一时间只有一个线程进入临界区,原子性是不会被调度打断的操作。
  • 线程安全问题根源:多线程并发操作共享资源,且操作不是原子的,会导致数据不一致、超卖等问题。
  • 互斥量的作用:通过加锁保证临界区代码的互斥执行,解决共享资源的并发访问问题。
  • 互斥量底层实现:基于 CPU 的 swap/exchange 原子指令,保证加锁操作的原子性,多核平台通过总线周期保证原子性。
  • 加锁注意事项:加锁粒度要最小化,所有临界区出口必须解锁,不能重复加锁。
  • RAII 锁封装:利用 C++ 对象的生命周期管理锁资源,构造加锁、析构解锁,避免漏解锁导致的死锁。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:如果本文对你有帮助,欢迎点赞、收藏、关注,下一篇我会继续讲解线程同步、条件变量、生产者消费者模型的核心内容,把 Linux 线程同步与互斥的知识体系完整讲透。有任何问题都可以在评论区留言,我会一一解答!

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
2301_764150562 小时前
CSS如何为目标锚点设置高亮样式_使用-target伪类定位当前模块
jvm·数据库·python
qq_342295822 小时前
HTML支持变量吗_与JavaScript数据绑定方式【解答】
jvm·数据库·python
feng_you_ying_li2 小时前
linux之进程概念
linux
j_xxx404_2 小时前
深入理解Linux底层存储:从物理磁盘架构到文件系统(inode/Block)原理
linux·运维·服务器·后端
逻辑驱动的ken2 小时前
Java高频面试场景题07
java·开发语言·面试·职场和发展·求职招聘·春招
j_xxx404_2 小时前
力扣算法题:字符串(最长公共前缀|最长回文子串)
c++·算法·leetcode
南棱笑笑生2 小时前
Z:\K7\20260418给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Buildroot时通过WinScp传送文件【SSH模式】
运维·ssh·rockchip
hutengyi2 小时前
四、nginx的优化和location匹配规则
运维·nginx
承渊政道2 小时前
【递归、搜索与回溯算法】(穷举vs暴搜vs深搜vs回溯vs剪枝:一文讲清概念与用法)
数据结构·c++·算法·决策树·深度优先·剪枝·宽度优先