跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

Linux 系统编程:线程互斥原理与实战指南

线程互斥是解决多线程共享资源竞争的关键。文章从共享资源与临界区概念入手,通过售票系统案例演示数据竞争现象,深入解析 Linux 互斥量(mutex)的初始化、加锁解锁及底层硬件原子指令原理。同时介绍 C++ RAII 风格封装,提供自动管理锁资源的方案,并涵盖死锁避免、过度加锁优化等实战避坑指南,助力开发者构建安全高效的并发程序。

山野来信发布于 2026/3/23更新于 2026/5/64 浏览
Linux 系统编程:线程互斥原理与实战指南

Linux 系统编程:线程互斥原理与实战指南

在多线程编程的世界里,线程互斥是绕不开的核心知识点。想象一下,多个线程同时操作同一个售票系统的余票变量,结果可能卖出负数的车票;多个线程同时修改同一个全局变量,最终的结果可能和预期天差地别。这些都是多线程并发访问共享资源引发的数据竞争问题,而线程互斥就是解决这类问题的关键。

本文将从核心概念出发,拆解临界资源、临界区、原子性等基础知识点,深入 Linux 下互斥量(mutex)的使用与实现原理,并通过实战案例完成从理论到实践的落地,最后讲解 C++ 下的 RAII 风格锁封装,让你不仅懂原理,还能写出优雅、安全的多线程代码。

核心概念:共享资源与临界区

在学习具体操作之前,先把几个核心概念吃透,这是理解后续所有内容的基础。

共享资源与临界资源

在多线程程序中,线程之间可以通过共享数据完成交互,这些被多个线程共同访问的数据就是共享资源。比如售票系统中的余票变量 ticket、电商系统中的库存变量 stock,都是典型的共享资源。

但并不是所有共享资源都需要特殊保护,需要被保护的共享资源才是临界资源。简单来说,临界资源是多线程执行流中,不能被多个线程同时访问的资源,一旦同时访问,就会引发数据不一致、结果异常等问题。

举个例子,售票系统的 ticket 变量是临界资源,因为多个售票线程同时对它进行'判断是否大于 0→打印余票→减 1'的操作时,会出现计算错误;而每个线程内部的局部变量只归当前线程所有,其他线程无法访问,就不是临界资源,无需保护。

临界区

有了临界资源,就对应有临界区。每个线程内部,访问临界资源的代码段,就是临界区。非临界区则是线程中不访问临界资源的代码,多个线程可以并发执行,无需限制。

比如售票线程中的这段代码,就是典型的临界区:

if (ticket > 0) { 
    usleep(1000); 
    printf("%s sells ticket:%d\n", id, ticket); 
    ticket--; 
}

这段代码直接访问了临界资源 ticket,是需要被互斥保护的核心代码段;而线程中其他的打印日志、局部变量计算等代码,都属于非临界区。

互斥的定义

搞懂了临界资源和临界区,就可以定义互斥了:任何时刻,保证有且只有一个执行流进入临界区,访问临界资源,这就是互斥。互斥的核心目的是对临界资源进行保护,避免多个线程同时操作临界资源引发的数据竞争问题。

原子性:互斥的底层要求

要实现有效的互斥,操作临界资源的代码必须满足原子性。原子性指的是不会被任何调度机制打断的操作,该操作只有两种状态:要么完成,要么未完成,不存在'执行了一半'的中间状态。

在多线程环境中,操作系统的线程调度是随机的(时间片轮转),如果一个操作不具备原子性,执行到一半时被其他线程抢占 CPU,就会导致临界资源的状态混乱。这也是多线程操作共享资源出问题的根本原因——对临界资源的操作不是原子操作。

先看个反面案例:多线程售票

光说概念不够直观,我们直接写一个经典的多线程售票系统案例,看看多个线程并发操作临界资源时,会出现什么样的问题。

问题代码:未加互斥的售票系统

我们创建 4 个售票线程,同时对全局变量 ticket(初始值 100)进行售票操作:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 临界资源:售票系统余票
int ticket = 100;

// 售票线程执行函数
void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        // 临界区:访问临界资源 ticket
        if (ticket > 0) {
            // 模拟售票的耗时业务(比如联网查询、打印车票)
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
    return NULL;
}

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;
}

编译运行与异常结果

将上述代码保存为 ticket.c,使用 gcc 编译(需要链接 pthread 库):

gcc ticket.c -o ticket -lpthread
./ticket

运行后会发现异常结果:余票会出现 0、-1、-2 等情况,比如:

thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

明明我们在代码中判断了 ticket > 0 才会售票,为什么会卖出负数的车票?这就是多线程并发访问临界资源的典型问题,我们来一步步拆解原因。

问题根源:三步分析

出现负数车票的原因主要有三点,层层递进,也是所有多线程共享资源问题的共性原因:

1. 线程调度的随机性

if (ticket > 0) 判断条件为真后,操作系统可能会将当前线程的 CPU 时间片收回,切换到其他线程。比如线程 1 判断 ticket=1 为真,还没执行 ticket--,就被切走了;此时线程 2、3、4 也判断 ticket=1 为真,都进入了售票逻辑,最终多个线程对 ticket 进行减 1 操作,就出现了负数。

2. 耗时操作放大了竞争问题

代码中的 usleep(1000) 模拟了售票的耗时业务,这让线程在临界区中停留的时间变长,线程调度的概率大大增加,使得数据竞争的问题更明显。即使去掉 usleep,由于线程调度的随机性,依然会出现问题,只是概率降低。

3. ticket--本身不是原子操作

这是最根本的原因:我们看似简单的 ticket-- 操作,在计算机底层并不是一条指令,而是对应三条汇编指令:

load:将共享变量 ticket 从内存加载到 CPU 寄存器中; update:在寄存器中执行减 1 操作,更新值; store:将寄存器中的新值写回 ticket 的内存地址。

我们可以通过 objdump 命令查看 ticket-- 的汇编代码来验证这一点。三条汇编指令意味着 ticket-- 可以被线程调度打断,执行到一半时被其他线程抢占,导致临界资源的状态混乱。

解决问题的核心要求

要解决上述问题,我们需要让临界区的代码满足三个核心要求,这也是设计互斥锁的基本准则:

互斥行为:当一个线程进入临界区执行时,不允许其他线程进入该临界区; 唯一准入:如果多个线程同时请求进入临界区,且临界区无线程执行,只能允许一个线程进入; 非阻塞无关:如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要满足这三个要求,本质上就是需要一把锁——在 Linux 下,这把锁就是互斥量(mutex)。

Linux 下的互斥量:mutex 的使用全解析

Linux 提供了一套完整的 POSIX 线程库(pthread),其中的**互斥量(pthread_mutex_t)**是实现线程互斥的核心工具。互斥量就像临界区的'大门钥匙',只有拿到钥匙的线程才能进入临界区,访问临界资源;其他线程只能等待,直到钥匙被归还。

接下来我们详细讲解互斥量的初始化、销毁、加锁、解锁等核心接口,以及如何用互斥量改造售票系统,解决数据竞争问题。

互斥量的类型与核心接口

互斥量的核心类型是 pthread_mutex_t,所有操作都围绕这个类型展开,核心接口包括初始化、销毁、加锁、解锁,均在 <pthread.h> 头文件中声明。

初始化互斥量

互斥量的初始化有两种方式:静态分配和动态分配,适用于不同的场景。

方式 1:静态分配

如果互斥量是全局变量或静态变量,可以直接使用宏 PTHREAD_MUTEX_INITIALIZER 初始化,简单高效,无需手动调用函数:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方式 2:动态分配

如果互斥量是局部变量或通过 malloc 在堆上分配的,需要使用 pthread_mutex_init 函数初始化:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数说明:

mutex:指向要初始化的互斥量对象的指针; attr:互斥量的属性,一般设为 NULL(使用默认属性)。 返回值:成功返回 0,失败返回对应的错误号。

销毁互斥量

互斥量使用完成后需要销毁,释放相关资源,核心函数是 pthread_mutex_destroy:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁互斥量的注意事项(非常重要):

静态分配的互斥量(用 PTHREAD_MUTEX_INITIALIZER 初始化)不需要手动销毁; 不要销毁一个已经加锁的互斥量,否则会导致未定义行为; 已经销毁的互斥量,要确保后续没有线程再尝试加锁。

加锁与解锁

互斥量的核心操作是加锁(pthread_mutex_lock) 和解锁(pthread_mutex_unlock),这两个操作是实现互斥的关键。

加锁函数
int pthread_mutex_lock(pthread_mutex_t *mutex);

功能:尝试获取互斥量的锁,如果互斥量未被锁定,则成功加锁,当前线程继续执行;如果互斥量已被其他线程锁定,则当前线程会阻塞(执行流被挂起),直到互斥量被解锁。

解锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);

功能:释放互斥量的锁,将互斥量置为未锁定状态;如果有其他线程因等待该互斥量而阻塞,会唤醒其中一个线程(由操作系统调度),让其尝试获取锁。

加锁 / 解锁的注意事项:

加锁和解锁必须成对出现,加锁后忘记解锁会导致死锁; 临界区的代码要尽可能精简,加锁后尽快解锁,减少线程阻塞的时间,提高程序并发效率; 加锁和解锁的调用必须在同一个线程中,不能在 A 线程加锁,B 线程解锁。

实战改造:加互斥量的售票系统

我们用互斥量改造之前的售票系统,将临界区的代码用加锁和解锁包裹,保证同一时刻只有一个线程进入临界区操作 ticket 变量。

改造后的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
// 全局互斥量,静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        // 加锁:进入临界区前获取锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            // 解锁:临界区代码执行完成,释放锁
            pthread_mutex_unlock(&mutex);
        } else {
            // 注意:else 分支也要解锁,否则会导致死锁
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 可选:让出 CPU,让其他线程有机会执行,提高并发度
        // sched_yield();
    }
    return NULL;
}

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);

    // 销毁互斥量(静态初始化可省略,这里为了规范)
    pthread_mutex_destroy(&mutex);
    return 0;
}
关键改造点说明

定义了全局互斥量 mutex,使用静态初始化方式,简单方便; 在进入临界区前调用 pthread_mutex_lock(&mutex) 加锁,保证只有一个线程能进入; 临界区代码执行完成后,立即调用 pthread_mutex_unlock(&mutex) 解锁; else 分支必须解锁:如果 ticket <= 0,线程会直接 break,若不解锁,互斥量会一直处于锁定状态,其他线程永远阻塞,导致死锁。

编译运行与正确结果

编译运行代码:

gcc ticket_mutex.c -o ticket_mutex -lpthread
./ticket_mutex

此时运行结果会完全符合预期,余票从 100 依次递减到 1,不会出现 0 或负数。

至此,我们成功通过互斥量解决了多线程售票系统的 data race 问题,实现了线程互斥。

互斥量的实现原理:为什么能保证原子性?

通过上面的案例,我们已经会用互斥量了,但作为程序员,我们需要知道互斥量本身是如何实现原子性的?毕竟互斥量的加锁操作也是对共享资源(互斥量自身的状态)的操作,如果加锁操作不是原子的,依然会出现竞争问题。

核心原理:硬件提供的原子指令

要实现互斥量的原子加锁 / 解锁,硬件层面为我们提供了原子交换指令(如 x86 架构的 xchgb 指令、ARM 架构的 swp 指令)。这些指令的特点是一条指令完成内存和寄存器的数据交换,而 CPU 的指令执行是原子的,不会被调度打断,这是互斥量实现的基础。

以 x86 的 xchgb(字节交换)指令为例,它能完成'将寄存器中的值和内存地址中的值交换'的操作,整个过程是原子的,即使在多处理器平台,访问内存的总线周期也有先后,一个处理器的交换指令执行时,另一个处理器的交换指令只能等待总线周期,保证了操作的唯一性。

互斥量的加锁 / 解锁伪代码实现

我们用伪代码模拟互斥量的 lock 和 unlock 操作,基于 xchgb 原子指令,让你更直观地理解原理(假设互斥量 mutex 的初始值为 1,1 表示未锁定,0 表示已锁定):

加锁操作(lock)
lock:
    movb $0, %al          ; 将寄存器 al 的值设为 0(表示锁定状态)
    xchgb %al, mutex      ; 原子交换:al 和 mutex 的内存值交换
    if (%al == 0) {
        ; 交换后如果 al 为 0,说明 mutex 原本是 0(已被锁定)
        挂起当前线程;     ; 线程阻塞,等待解锁
        goto lock;         ; 被唤醒后重新尝试加锁
    }
    return 0;              ; 交换后 al 为 1,说明加锁成功

加锁逻辑解析:

线程先将寄存器设为 0,代表'想要锁定'; 通过原子交换指令,将寄存器的值和互斥量的内存值交换; 如果交换后寄存器的值为 0,说明互斥量原本是 0(已被其他线程锁定),当前线程挂起等待; 如果交换后寄存器的值为 1,说明互斥量原本是 1(未锁定),加锁成功,线程进入临界区。

解锁操作(unlock)
unlock:
    movb $1, mutex        ; 将互斥量的内存值设为 1(未锁定状态),原子操作
    唤醒等待 mutex 的线程   ; 唤醒因该互斥量阻塞的线程,让其重新尝试加锁
    return 0;

解锁逻辑解析:

直接将互斥量的值设为 1,恢复未锁定状态(赋值操作是原子的); 唤醒等待该互斥量的线程,让这些线程重新进入加锁的竞争流程。

核心总结

互斥量的实现依赖硬件的原子指令,保证了加锁操作的原子性;而互斥量的加锁 / 解锁又保证了临界区代码的原子性,从而实现了对临界资源的保护。简单来说:硬件原子指令保证了锁的安全性,锁保证了临界区的安全性。

C++ 封装互斥量:RAII 风格让锁更安全、更优雅

在 C 语言中,我们需要手动调用 pthread_mutex_lock 和 pthread_mutex_unlock,必须保证成对出现,否则容易出现死锁。而在 C++ 中,我们可以利用RAII(资源获取即初始化) 思想,对互斥量进行封装,让锁的生命周期和对象的生命周期绑定,自动加锁、自动解锁,从根本上避免忘记解锁的问题。

RAII 的核心思想是:在对象构造时获取资源,在对象析构时释放资源。因为 C++ 的对象析构是自动的(无论正常退出还是异常退出,对象超出作用域时都会调用析构函数),所以基于 RAII 封装的锁,能保证锁一定会被释放,极大地提高了代码的安全性和优雅性。

接下来我们实现一个 C++ 的互斥量封装,包括基础的 Mutex 类和RAII 风格的 LockGuard 类,并改造售票系统为 C++ 版本。

封装互斥量:Lock.hpp 头文件

我们将封装的代码写在 Lock.hpp 头文件中,实现跨文件复用:

#pragma once
#include <iostream>
#include <pthread.h>
#include <cassert>

// 命名空间,避免命名冲突
namespace LockModule {
    // 封装基础的互斥量类
    class Mutex {
    public:
        // 禁止拷贝和赋值(互斥量不能被拷贝)
        Mutex(const Mutex &) = delete;
        const Mutex &operator=(const Mutex &) = delete;

        // 构造函数:动态初始化互斥量
        Mutex() {
            int ret = pthread_mutex_init(&_mutex, nullptr);
            assert(ret == 0);
            (void)ret;
        }

        // 加锁
        void Lock() {
            int ret = pthread_mutex_lock(&_mutex);
            assert(ret == 0);
            (void)ret;
        }

        // 解锁
        void Unlock() {
            int ret = pthread_mutex_unlock(&_mutex);
            assert(ret == 0);
            (void)ret;
        }

        // 获取原生的 pthread_mutex_t 指针(供条件变量等使用)
        pthread_mutex_t *GetMutexOriginal() {
            return &_mutex;
        }

        // 析构函数:销毁互斥量
        ~Mutex() {
            int ret = pthread_mutex_destroy(&_mutex);
            assert(ret == 0);
            (void)ret;
        }

    private:
        pthread_mutex_t _mutex;
    };

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

        // 析构函数:对象析构时解锁
        ~LockGuard() {
            _mutex.Unlock();
        }

        // 禁止拷贝和赋值
        LockGuard(const LockGuard &) = delete;
        const LockGuard &operator=(const LockGuard &) = delete;

    private:
        Mutex &_mutex;
    };
}

封装代码解析

Mutex 类:基础互斥量封装

封装了原生的 pthread_mutex_t,对外提供 Lock()、Unlock()、GetMutexOriginal() 接口,隐藏底层实现; 禁止拷贝和赋值:互斥量是独占资源,不能被拷贝,通过 = delete 禁用拷贝构造函数和赋值运算符; 构造函数初始化互斥量,析构函数销毁互斥量,保证资源的正确管理; 使用 assert 做断言检查,调试阶段能快速发现互斥量操作的错误,release 版本中 assert 会被屏蔽,不影响性能。

LockGuard 类:RAII 锁守卫

构造函数接收 Mutex 对象的引用(避免拷贝),并立即调用 _mutex.Lock() 加锁; 析构函数调用 _mutex.Unlock() 解锁,对象超出作用域时自动执行; 同样禁止拷贝和赋值,保证锁的唯一性; 使用 explicit 关键字修饰构造函数,避免隐式类型转换,提高代码可读性。

C++ 版本实战:RAII 锁改造售票系统

我们用封装的 Mutex 和 LockGuard 改造售票系统,代码更简洁、更安全,无需手动调用加锁和解锁:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"

using namespace LockModule;

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;
    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);

    // 无需手动销毁互斥量,mutex 对象析构时自动销毁
    return 0;
}

代码优势分析

无需手动加锁 / 解锁:只需定义 LockGuard 对象,构造时自动加锁,析构时自动解锁,即使临界区出现 break、return 或异常,也能保证解锁; 代码更简洁:临界区的代码无需被加锁 / 解锁函数包裹,逻辑更清晰; 彻底避免死锁:从根本上解决了'忘记解锁'的问题,这是 C++ RAII 封装的最大优势; 可复用性强:封装的 Mutex 和 LockGuard 可以在任何多线程程序中使用,无需重复编写底层代码。

补充:C++11 标准库中已经提供了原生的互斥量和 RAII 锁,分别是 std::mutex 和 std::lock_guard,用法和我们封装的类几乎一致,底层也是基于平台的互斥量实现(Linux 下是 pthread_mutex_t)。我们自己封装的目的是为了更深入地理解 RAII 的原理,实际开发中可以直接使用 C++11 的标准库。

常见问题与避坑指南

在实际开发中,使用互斥量不仅要懂原理、会用接口,还要避开常见的坑,否则容易出现死锁、性能低下、过度加锁等问题。

死锁(最严重的问题)

死锁是多线程编程中最严重的问题之一,指的是多个线程互相持有对方需要的锁,且都不释放自己的锁,导致所有线程永远阻塞。

产生条件

死锁的产生必须同时满足四个必要条件,缺一不可:

互斥条件:一个资源每次只能被一个线程使用; 请求与保持条件:线程持有已获取的锁,同时请求其他线程持有的锁,且不释放自己的锁; 不剥夺条件:线程已获取的锁,在未使用完之前,不能被其他线程强行剥夺; 循环等待条件:多个线程形成头尾相接的循环,每个线程都等待下一个线程持有的锁。

避免方法

避免死锁的核心是破坏死锁的四个必要条件之一,其中破坏循环等待条件是最常用、最易实现的方法:

统一加锁顺序:所有线程获取多个锁时,按照固定的顺序加锁; 一次性获取所有锁:使用 pthread_mutex_lock 的扩展接口(或 C++11 的 std::lock),一次性获取多个锁; 设置锁的超时时间:使用 pthread_mutex_timedlock,如果在指定时间内未获取到锁,就放弃并释放已持有的锁; 避免嵌套加锁:尽量减少锁的嵌套使用,嵌套加锁是死锁的高发场景; 加锁后尽快解锁:减少线程持有锁的时间,降低锁竞争的概率。

过度加锁导致并发效率低下

互斥的本质是牺牲部分并发效率,保证数据安全,但如果过度加锁,会导致程序的并发效率急剧下降,甚至退化为串行执行。

优化技巧

临界区最小化:只对访问临界资源的核心代码加锁,非临界区的代码尽量放在锁外; 细粒度加锁:将大的临界资源拆分为多个小的临界资源,为每个小资源分配独立的互斥量; 避免无意义的加锁:只对临界资源的操作加锁,线程局部变量、只读共享资源无需加锁; 使用自旋锁替代互斥量:如果临界区的代码执行时间极短,可以使用自旋锁(Linux 下的 pthread_spinlock_t),自旋锁不会让线程阻塞,而是一直尝试获取锁,直到成功。

锁的拷贝与赋值

互斥量是独占资源,不能被拷贝或赋值,因为拷贝互斥量会导致多个互斥量对象指向同一个底层资源,或创建一个新的互斥量对象,破坏互斥的语义,引发未定义行为。

避坑方法

在 C++ 中,通过 = delete 显式禁用互斥量类的拷贝构造函数和赋值运算符; 在 C 语言中,避免将互斥量作为函数参数值传递(值传递会拷贝),应使用指针或引用传递; 不要将互斥量放入容器中(容器的操作会涉及拷贝),如果需要,应放入容器的指针或智能指针。

在信号处理函数中使用互斥量

在 Linux 中,信号处理函数中不能使用 pthread 的互斥量,因为 pthread 的互斥量是可重入的,但信号处理函数的执行是异步的,可能会导致死锁。

避坑方法

信号处理函数中尽量只做简单的操作,比如设置标志位,不要访问临界资源; 如果需要在信号处理函数中访问临界资源,使用 Linux 提供的信号量(sem_t) 或原子变量,而不是互斥量; 避免在持有互斥量时触发信号处理函数。

应用场景

线程互斥是多线程编程的基础,几乎所有的多线程共享资源场景都需要用到线程互斥,以下是一些典型的应用场景:

售票系统 / 库存系统:多个线程同时修改余票 / 库存变量,需要互斥保护; 银行转账系统:多个线程同时操作账户余额,需要保证转账操作的原子性; 日志系统:多个线程同时向日志文件写入内容,需要保证日志的完整性,避免内容错乱; 缓存系统:多个线程同时读写缓存(如内存中的哈希表),需要互斥保护缓存数据; 生产消费模型:生产者和消费者线程同时操作阻塞队列,需要互斥保护队列的入队 / 出队操作; 线程池:多个工作线程同时从任务队列中获取任务,需要互斥保护任务队列。

简单来说,只要多个线程同时访问并修改同一个共享资源,就需要使用线程互斥。而对于只读的共享资源,无需互斥保护,因为只读操作不会改变资源的状态,不会引发数据竞争。

总结

线程互斥是多线程同步的基础,但实际开发中,除了互斥,我们还需要线程同步(让线程按照特定的顺序执行),比如生产消费模型中,生产者生产数据后,需要通知消费者消费;消费者消费完数据后,需要通知生产者生产。掌握互斥机制,是构建高可靠并发系统的基石。

目录

  1. Linux 系统编程:线程互斥原理与实战指南
  2. 核心概念:共享资源与临界区
  3. 共享资源与临界资源
  4. 临界区
  5. 互斥的定义
  6. 原子性:互斥的底层要求
  7. 先看个反面案例:多线程售票
  8. 问题代码:未加互斥的售票系统
  9. 编译运行与异常结果
  10. 问题根源:三步分析
  11. 1. 线程调度的随机性
  12. 2. 耗时操作放大了竞争问题
  13. 3. ticket--本身不是原子操作
  14. 解决问题的核心要求
  15. Linux 下的互斥量:mutex 的使用全解析
  16. 互斥量的类型与核心接口
  17. 初始化互斥量
  18. 方式 1:静态分配
  19. 方式 2:动态分配
  20. 销毁互斥量
  21. 加锁与解锁
  22. 加锁函数
  23. 解锁函数
  24. 实战改造:加互斥量的售票系统
  25. 改造后的代码
  26. 关键改造点说明
  27. 编译运行与正确结果
  28. 互斥量的实现原理:为什么能保证原子性?
  29. 核心原理:硬件提供的原子指令
  30. 互斥量的加锁 / 解锁伪代码实现
  31. 加锁操作(lock)
  32. 解锁操作(unlock)
  33. 核心总结
  34. C++ 封装互斥量:RAII 风格让锁更安全、更优雅
  35. 封装互斥量:Lock.hpp 头文件
  36. 封装代码解析
  37. Mutex 类:基础互斥量封装
  38. LockGuard 类:RAII 锁守卫
  39. C++ 版本实战:RAII 锁改造售票系统
  40. 代码优势分析
  41. 常见问题与避坑指南
  42. 死锁(最严重的问题)
  43. 产生条件
  44. 避免方法
  45. 过度加锁导致并发效率低下
  46. 优化技巧
  47. 锁的拷贝与赋值
  48. 避坑方法
  49. 在信号处理函数中使用互斥量
  50. 避坑方法
  51. 应用场景
  52. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 前端 PWA:构建离线可用与可安装的应用
  • 七款主流大模型英文降重能力横向测评
  • Figma设计稿转前端代码:基于Cursor IDE MCP功能的自动化方案
  • LangChain 快速入门指南:从基础组件到服务部署
  • QUEST 一体机本地游戏安装指南:SideQuest 使用详解
  • GitHub Copilot 配置避坑指南与常见错误分析
  • Docker CE 在 Kali/Ubuntu 系统上的安装与配置指南
  • MiniMax-M2.5 开源模型发布:编程与智能体性能评测
  • 基于 SSM 框架与 Vue 的在线投稿系统设计与实现
  • LeetCode 热题 100 算法回顾
  • Claude Code 安装与使用指南:配置、命令及 IDE 集成
  • JDK 安装与环境配置详解
  • 基于 YOLO 标注格式的无人机航拍车辆识别检测数据集
  • 大疆无人机反制手段解析:干扰枪与激光武器效果对比
  • 2025 年世界职业院校技能大赛人工智能赛道备赛方案
  • Mac Mini M4 本地运行大模型:Ollama 与 Llama 环境搭建
  • 本地部署 Z-Image-Turbo:16GB 显存实现高效 AI 绘画
  • 使用 GitHub Copilot 配合 Figma MCP 还原设计稿生成前端代码
  • 昇腾 NPU 部署 Llama 2 模型的性能测试与优化实践
  • 网络安全入门:成为安全从业者的 12 个关键步骤

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online