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;
};
相关推荐
NE_STOP10 小时前
Vide Coding--AI编程工具的选择
java
大树8810 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠10 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
码云数智-园园10 小时前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆10 小时前
JDK 下载安装与配置详细教程
java·前端·ai编程
霸道流氓气质10 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush410 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52010 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz11 小时前
Maven依赖冲突
java·服务器·maven
swordbob11 小时前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio