Linux 学习笔记(三十三):线程互斥——互斥锁(Mutex)与死锁

一、 为什么多线程修改全局变量会出错?

在 C/C++ 中,ticket-- 并不是一个原子操作。它在汇编层面对应三条指令(load, update, store)。

当多个线程并发执行时,CPU 的时间片切换可能发生在任意一步。这就导致线程 A 读了数据还没写回,线程 B 又去读,最终互相覆盖。这种现象叫做竞态条件(Race Condition)。

二、 互斥锁(Mutex)

为了解决并发冲突,引入了互斥锁。它保证了在任意时刻,只能有一个线程进入临界区(访问共享资源的代码)。

  • pthread_mutex_lock():加锁。如果已被别人加锁,当前线程阻塞。

  • pthread_mutex_unlock():解锁。

三、 高阶封装:RAII 风格的锁守卫

手动调用 lock 和 unlock 极易因为程序抛出异常或中途 break 导致"死锁"。

C++ 采用 RAII(资源获取即初始化)机制:在构造函数中加锁,在析构函数中解锁。只要对象生命周期结束,就一定会解锁。极其优雅且安全。

四、 死锁的四个必要条件

用锁不当会导致死锁(两个线程互等)。死锁发生必须具备四个条件:

  1. 互斥:资源同一时间只能被一个线程使用。

  2. 请求与保持:拿着一个锁,去等另一个锁,且不释放手里的。

  3. 不剥夺:别人不能强行抢锁。

  4. 循环等待 :存在一个线程资源等待的环形链(A等B,B等A)。

    避免方法 :破坏其中任意一个条件即可,最常见的是所有线程按相同的顺序申请锁(破坏循环等待)。

自我实战操作

bash 复制代码
cd ~/linux_study
mkdir mutex_test
cd mutex_test
实战 1:灾难现场(不加锁)
bash 复制代码
vim ticket_no_lock.cc

(输入以下代码)

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

int ticket = 100; // 100张票

// 抢票动作
void* route(void* arg) {
    char* name = (char*)arg;
    while (true) {
        if (ticket > 0) {
            usleep(1000); // 模拟抢票时的网络延迟等耗时操作
            printf("%s 卖出一张票,剩余: %d\n", name, ticket);
            ticket--;
        } else {
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    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;
}

编译并运行:

bash 复制代码
g++ ticket_no_lock.cc -o ticket_no_lock -lpthread
./ticket_no_lock | tail -n 10 # 只看最后10行

👉 现象 :你会看到最后卖出的票数变成了 0、-1、-2 甚至更低。这就是典型的多线程数据冲突。

实战 2:引入 RAII 互斥锁

极其标准的 Lock.hpp 封装,它利用了 C++ 的析构函数自动解锁,防止我们忘了解锁导致死锁。

bash 复制代码
vim Lock.hpp

(输入以下代码)

cpp 复制代码
#pragma once
#include <pthread.h>

class Mutex {
public:
    Mutex() { pthread_mutex_init(&_mutex, nullptr); }
    ~Mutex() { pthread_mutex_destroy(&_mutex); }
    void Lock() { pthread_mutex_lock(&_mutex); }
    void Unlock() { pthread_mutex_unlock(&_mutex); }
private:
    pthread_mutex_t _mutex;
    // 禁止拷贝
    Mutex(const Mutex&) = delete;
    const Mutex& operator=(const Mutex&) = delete;
};

// RAII 风格的锁守卫
class LockGuard {
public:
    LockGuard(Mutex& mutex) : _mutex(mutex) { _mutex.Lock(); }
    ~LockGuard() { _mutex.Unlock(); }
private:
    Mutex& _mutex;
};
实战 3:完美解决冲突

修改刚才的抢票代码,引入 Lock.hpp:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "Lock.hpp" // 引入封装好的锁

int ticket = 100;
Mutex mtx; // 全局的互斥锁

void* route(void* arg) {
    char* name = (char*)arg;
    while (true) {
        // 利用 RAII,在进入 if 之前加锁,离开作用域(或 break)时自动解锁
        LockGuard guard(mtx); 
        
        if (ticket > 0) {
            usleep(1000);
            printf("%s 卖出一张票,剩余: %d\n", name, ticket);
            ticket--;
        } else {
            break;
        }
    }
    return nullptr;
}
// main 函数和上面一样

重新编译运行:

bash 复制代码
g++ ticket_lock.cc -o ticket_lock -lpthread
./ticket_lock

👉 现象:票数完美变成 0 结束,没有任何负数,且 4 个线程井然有序。

相关推荐
无足鸟ICT1 小时前
【RHCA+】who命令(查看已登陆用户)
linux·运维·服务器
Escalating_xu1 小时前
【Linux 网络编程】应用层协议 HTTP(下):Header、连接管理与 C++ HTTP 服务器
linux·服务器·网络
RacentCLM1 小时前
证书即服务CaaS:从申请、续期到部署的自动化链路拆解
运维·数据库·自动化·ssl
注塑机数据采集联网2 小时前
注塑机生产管控MES系统应用
大数据·运维·服务器·人工智能·物联网
云和数据.ChenGuang2 小时前
Python 常见特殊方法详解:__new__、__init__、__del__、__str__、__repr__ 与 __getattribute__
开发语言·人工智能·python·1024程序员节
海宇服务2 小时前
零信任架构实战:基于海宇全国车辆配置查验构建自动化二手商用车出厂参数建档网关
运维·人工智能·架构·自动化·1024程序员节
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(113):Agentic Memory Management——从“检索相关信息”走向“管理 Agent 内部记忆”
论文阅读·人工智能·学习·开源·github
深圳老胡3 小时前
STM32C5A3ZG 网络调试总结:从启动失败到连续 Ping 稳定
笔记·stm32·单片机·代码规范