Linux:线程互斥

在多核 CPU 时代,多线程并发是提升程序性能的关键,但线程间对共享资源的 "争抢" 往往会导致数据错乱、结果异常等问题。线程互斥技术通过 "锁" 机制,保证同一时刻只有一个线程能访问临界资源,是解决并发冲突的核心方案。本文从问题本质出发,带你理解线程互斥的原理、工具使用与工程实践。

一、核心问题:为什么需要线程互斥?

多线程并发访问临界资源(如全局变量、文件、网络连接等被多个线程共享的资源)时,若缺乏保护,会因操作非原子性导致数据一致性问题。最经典的案例就是 "多线程售票系统":

复制代码
// 无锁售票系统:存在数据竞争问题
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int ticket = 100; // 临界资源:剩余票数

void* sellTicket(void* arg) {
    char* threadId = (char*)arg;
    while (1) {
        if (ticket > 0) {
            usleep(1000); // 模拟购票业务耗时
            printf("%s 卖出票号:%d\n", threadId, ticket--);
        } else {
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, sellTicket, (void*)"线程1");
    pthread_create(&t2, nullptr, sellTicket, (void*)"线程2");
    pthread_create(&t3, nullptr, sellTicket, (void*)"线程3");
    pthread_create(&t4, nullptr, sellTicket, (void*)"线程4");
    
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

运行结果异常分析

执行后可能出现 "超卖"(卖出 0 号、-1 号票),原因是 ticket-- 并非原子操作,实际对应三条汇编指令:

  1. Load :将内存中的 ticket 加载到寄存器;
  2. Update:寄存器中的值减 1;
  3. Store:将新值写回内存。

线程调度可能发生在任意指令之间,比如线程 1 执行完 Load 后被切换,线程 2 同样加载 ticket=1,最终两个线程都执行 Store,导致 ticket 从 1 变成 0 而非 - 1,出现数据错乱

二、互斥核心概念

要解决并发冲突,需先明确三个核心概念:

  • 临界资源 :多线程共享且需保护的资源(如上述的ticket
  • 临界区 :访问临界资源的代码段(如if (ticket > 0) { ... ticket--; }
  • 原子性:操作要么完整执行,要么不执行,不会被调度机制打断
  • 互斥:保证同一时刻只有一个线程进入临界区,避免资源竞争

实现互斥的核心工具是互斥量(Mutex),它就像临界区的 "门锁",线程进入前需 "上锁",退出后 "解锁",未抢到锁的线程会阻塞等待

三、互斥量的使用(POSIX pthread 库)

Linux 系统提供了 pthread 系列接口操作互斥量,核心流程为 "初始化→加锁→访问临界区→解锁→销毁"。

1. 核心接口

接口函数 功能描述 关键说明
pthread_mutex_init(pthread_mutex_t* mutex, nullptr) 动态初始化互斥量 第二个参数为属性,传 nullptr 使用默认属性
pthread_mutex_lock(pthread_mutex_t* mutex) 加锁 若锁已被占用,线程阻塞等待
pthread_mutex_unlock(pthread_mutex_t* mutex) 解锁 只能解锁当前线程持有的锁
pthread_mutex_destroy(pthread_mutex_t* mutex) 销毁互斥量 仅能销毁未加锁的互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 静态初始化 全局 / 静态互斥量推荐使用

2. 修复售票系统:加互斥量保护

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

int ticket = 100;
pthread_mutex_t mutex; // 互斥量

void* sellTicket(void* arg) {
    char* threadId = (char*)arg;
    while (1) {
        pthread_mutex_lock(&mutex); // 加锁:进入临界区
        if (ticket > 0) {
            usleep(1000);
            printf("%s 卖出票号:%d\n", threadId, ticket--);
            pthread_mutex_unlock(&mutex); // 解锁:退出临界区
        } else {
            pthread_mutex_unlock(&mutex); // 解锁后再退出
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_mutex_init(&mutex, nullptr); // 初始化互斥量
    
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, sellTicket, (void*)"线程1");
    pthread_create(&t2, nullptr, sellTicket, (void*)"线程2");
    pthread_create(&t3, nullptr, sellTicket, (void*)"线程3");
    pthread_create(&t4, nullptr, sellTicket, (void*)"线程4");
    
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    
    pthread_mutex_destroy(&mutex); // 销毁互斥量
    return 0;
}

加锁后,临界区被互斥保护,同一时刻只有一个线程能修改ticket,不会出现超卖问题

四、工程优化:RAII 风格封装互斥量

手动加解锁容易因异常、return 导致锁未释放(死锁风险),推荐用 C++ 的 RAII(资源获取即初始化)风格封装,让锁的生命周期与对象绑定,自动管理加解锁

封装代码(Lock.hpp)

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

namespace LockModule {
class Mutex {
public:
    // 禁用拷贝构造和赋值(避免多个对象管理同一把锁)
    Mutex(const Mutex&) = delete;
    Mutex& operator=(const Mutex&) = delete;

    Mutex() {
        pthread_mutex_init(&_mutex, nullptr); // 初始化互斥量
    }

    ~Mutex() {
        pthread_mutex_destroy(&_mutex); // 销毁互斥量
    }

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

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

    // 获取原始互斥量指针(用于条件变量等场景)
    pthread_mutex_t* GetRawMutex() {
        return &_mutex;
    }

private:
    pthread_mutex_t _mutex;
};

// 自动加解锁:构造加锁,析构解锁
class LockGuard {
public:
    LockGuard(Mutex& mutex) : _mutex(mutex) {
        _mutex.Lock();
    }

    ~LockGuard() {
        _mutex.Unlock();
    }

private:
    Mutex& _mutex; // 引用传递,避免拷贝
};
}

使用封装后的锁优化售票系统

复制代码
#include "Lock.hpp"
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

using namespace LockModule;

int ticket = 100;
Mutex mutex; // 封装后的互斥量

void* sellTicket(void* arg) {
    char* threadId = (char*)arg;
    while (1) {
        LockGuard lock(mutex); // 构造时自动加锁
        if (ticket > 0) {
            usleep(1000);
            printf("%s 卖出票号:%d\n", threadId, ticket--);
        } else {
            break; // 析构时自动解锁
        }
    }
    return nullptr;
}

// main函数与之前一致,略...

LockGuard 对象生命周期结束时(退出循环或函数返回),析构函数自动解锁,彻底避免漏解锁问题

五、互斥量实现原理

互斥量的核心是保证 "加锁" 操作的原子性。大多数 CPU 提供swapexchange指令,该指令能原子地交换寄存器和内存的数据,基于此实现锁的逻辑

  • 加锁(Lock):将寄存器值设为 0,与互斥量内存地址交换;若交换后寄存器值 > 0,说明锁已被占用,线程阻塞
  • 解锁(Unlock):将互斥量内存地址设为 1,唤醒等待锁的线程

这种基于硬件指令的实现,保证了多处理器环境下的互斥安全性

六、使用互斥量的注意事项

  1. 锁粒度适中:临界区应仅包含访问临界资源的必要代码,避免扩大锁范围导致性能下降
  2. 避免死锁:多个线程加锁时,需保证加锁顺序一致(如都先锁 A 再锁 B),避免循环等待
  3. 禁止锁嵌套:同一线程对同一互斥量重复加锁会导致死锁
  4. 解锁时机:临界区执行完毕或异常退出前,必须解锁(RAII 封装可避免此问题)

总结

线程互斥的核心是通过互斥量保护临界资源,保证操作原子性,解决并发冲突。从原生接口到 RAII 封装,不仅是代码优雅度的提升,更是工程安全性的保障。掌握互斥量的使用,是编写线程安全代码的基础,也是后续学习更复杂并发模型的前提

下面提供一个互斥相关的代码,感兴趣的可以看看

testMutex.cc

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

int ticket = 1000;
pthread_mutex_t glock; //也可以使用全局锁

class PthreadData
{
public:
    PthreadData(std::string name, pthread_mutex_t& lock)
        : _name(name)
        , plock(&lock)
    {}
    ~PthreadData()
    {}
    std::string _name;
    pthread_mutex_t* plock;
};

void *route(void *arg)
{
    PthreadData* pthead_data = (PthreadData*)arg;
    while (1)
    {
        pthread_mutex_lock(pthead_data->plock);
        if (ticket > 0)
        {            
            usleep(1000);
            printf("%s sells ticket:%d\n", pthead_data->_name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(pthead_data->plock);
        }
        else
        {
            pthread_mutex_unlock(pthead_data->plock);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
    pthread_t t1, t2, t3, t4;
    PthreadData* p1 = new PthreadData("thread 1", lock);
    PthreadData* p2 = new PthreadData("thread 2", lock);
    PthreadData* p3 = new PthreadData("thread 3", lock);
    PthreadData* p4 = new PthreadData("thread 4", lock);
    pthread_create(&t1, NULL, route, (void *)p1);
    pthread_create(&t2, NULL, route, (void *)p2);
    pthread_create(&t3, NULL, route, (void *)p3);
    pthread_create(&t4, NULL, route, (void *)p4);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

Test.cc

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

int count = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* routine(void* args)
{
    while (true)
    {      
        pthread_mutex_lock(&lock);
        if (count > 0)
        {
            usleep(1000);
            std::cout << "count : " << count-- << std::endl;
            pthread_mutex_unlock(&lock);
        }
        else 
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}

int main()
{    
    std::vector<pthread_t> v;
    for (int i = 0; i < 4; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, routine, nullptr);
        v.push_back(tid);
    }
    for (auto e : v)
    {
        pthread_join(e, nullptr);
    }
    std::cout << "********count : " << count << std::endl;
    return 0;
}

Makefile:

bash 复制代码
code : testMutex.cc
	g++ $^ -o $@ -l pthread
.THONY : clean
clean : 
	rm -f code

线程互斥封装:

cpp 复制代码
#pragma once

#include <iostream>
#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;
};

class MutexGuard
{
public:
    MutexGuard(Mutex& mutex)
        : _mutex(mutex)
    {
        _mutex.Lock();
    }
    ~MutexGuard()
    {
        _mutex.UnLock();
    }
private:
    Mutex& _mutex;
};
相关推荐
xiaoye20181 小时前
Lettuce连接模型、命令执行、Pipeline 浅析
java
Sheffield4 小时前
Alpine是什么,为什么是Docker首选?
linux·docker·容器
beata4 小时前
Java基础-18:Java开发中的常用设计模式:深入解析与实战应用
java·后端
Seven975 小时前
剑指offer-81、⼆叉搜索树的最近公共祖先
java
舒一笑1 天前
程序员效率神器:一文掌握 tmux(服务器开发必备工具)
运维·后端·程序员
Johny_Zhao1 天前
centos7安装部署openclaw
linux·人工智能·信息安全·云计算·yum源·系统运维·openclaw
雨中飘荡的记忆1 天前
保证金系统入门到实战
java·后端
Nyarlathotep01131 天前
Java内存模型
java
haibindev1 天前
在 Windows+WSL2 上部署 OpenClaw AI员工的实践与踩坑
linux·wsl2·openclaw