Linux 线程互斥
进程线程间的互斥相关概念详解
共享资源
共享资源是指在多线程或多进程环境下,可以被多个执行流(线程或进程)共同访问的系统资源。这些资源可以是:
- 内存区域
- 文件
- 硬件设备
- 数据库记录
- 变量或数据结构
临界资源
临界资源是指那些在多线程执行流中需要被保护的共享资源。它们具有以下特点:
- 共享性:可以被多个线程访问
- 排他性:在某一时刻只能被一个线程使用
- 重要性:对这些资源的并发访问可能导致数据不一致或其他问题
例子:
- 共享内存中的计数器变量
- 打印机设备
- 数据库中的某条记录
临界区
临界区是指每个线程内部访问临界资源的代码段。关键点包括:
- 位置:是代码的一部分,不是数据
- 功能:包含对临界资源的访问或修改操作
- 要求:需要确保同一时间只有一个线程能执行这段代码
例子:
// 临界区开始
pthread_mutex_lock(&mutex);
balance += amount; // 访问共享变量balance
pthread_mutex_unlock(&mutex);
// 临界区结束
互斥
互斥是一种保证机制,确保在任何时刻只有一个执行流能够进入临界区并访问临界资源。互斥的特性包括:
- 排他性:一次只允许一个线程访问
- 必要性:防止竞态条件(race condition)
- 实现方式:通常通过锁机制实现
互斥的基本原则:
- 空闲让进:当没有线程在临界区时,应允许一个请求进入的线程进入
- 忙则等待:当已有线程在临界区时,其他试图进入的线程必须等待
- 有限等待:等待进入临界区的线程应该在有限时间内获得机会
- 让权等待(可选):等待的线程应释放CPU,避免忙等待
原子性
原子性是指操作不可被中断的特性,即一个操作要么完全执行,要么完全不执行,不会出现执行到一半被中断的情况。关键点:
- 不可分割:操作作为一个不可分割的单元执行
- 状态二元:只有”完成”或”未完成”两种状态,没有中间状态
- 重要性:是保证线程安全的基础
实现原子性的方法:
- 硬件支持的原子指令(如x86的
LOCK前缀指令) - 使用互斥锁(mutex)保护非原子操作
- 事务内存(某些高级语言支持)
例子:
// 非原子操作(在多线程环境下可能出问题)
counter++;
// 使用原子操作(C11标准)
atomic_fetch_add(&counter, 1);
互斥的实现方式
在实际编程中,实现互斥的常见方法包括:
- 互斥锁(Mutex):最基本的互斥机制
- 信号量(Semaphore):更通用的同步机制
- 自旋锁(Spinlock):适用于短时间等待的场景
- 读写锁(Read-Write Lock):区分读写操作
- 条件变量(Condition Variable):配合互斥锁使用
互斥锁 Mutex
背景引入
线程数据的天然隔离
在大多数情况下,线程使用的数据都是局部变量,这些变量的地址空间位于线程的私有栈空间中。就像每个人都有自己的私人笔记本一样,这些局部变量:
- 完全归属于创建它的线程
- 生命周期与线程执行周期绑定
- 其他线程根本无法直接访问
- 天然具备线程安全性
这种隔离性是线程独立运作的基础,也是多线程编程中最理想的数据使用方式。
我们引入一个示例:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <iostream>
using namespace std; // 使用标准命名空间
const int thread_num = 10; // 定义线程数量为10
int tickets = 1000; // 共享变量:总票数1000张
// 线程数据结构体,用于传递线程信息
struct thread_data {
pthread_t tid; // 线程ID
string name; // 线程名称
};
// 线程执行函数
void* thread_routine(void* data) {
// 将void*参数转换为thread_data*类型
thread_data* td = static_cast<thread_data*>(data);
// 获取当前线程ID
td->tid = pthread_self();
// 循环抢票
while (true) {
// 检查是否还有票
if (tickets > 0) {
usleep(1000); // 模拟业务处理延迟(1毫秒)
tickets--; // 票数减1(临界区操作)
// 输出抢票信息
cout << td->name << " get a ticket, "
<< "remaining tickets: " << tickets << endl;
} else {
break; // 没有票了,退出循环
}
}
delete td; // 释放线程数据内存
return nullptr; // 线程返回空指针
}
int main() {
vector<pthread_t> tids; // 存储线程ID
// 创建10个线程
for (int i = 1; i <= thread_num; ++i) {
string thread_name = "thread-" + to_string(i); // 生成线程名称
pthread_t tid; // 线程ID变量
// 创建线程数据对象
thread_data* td = new thread_data{
name: thread_name // 初始化线程名称
};
// 创建线程
pthread_create(&tid, nullptr, thread_routine, td);
tids.push_back(tid); // 将线程ID加入向量
}
// 等待所有线程结束
for (auto tid : tids) {
pthread_join(tid, nullptr);
}
return 0; // 主线程退出
}
共享变量的现实需求
然而,纯粹的隔离无法满足现实编程需求。想象一个团队协作的场景:我们需要多个线程共同处理同一批数据,或者需要线程间传递处理结果。这时就必须引入共享变量:
// 共享的票务资源
int tickets = 1000;
共享变量就像办公室里的公共白板:
- 所有团队成员(线程)都能看到并修改
- 是线程间通信的重要渠道
- 可以实现数据共享和协作处理
并发操作引发的混乱
但当我们允许多个线程同时操作共享变量时,问题开始显现。以票务系统为例:
if(tickets > 0){ // 检查票数
usleep(1000); // 模拟业务处理延迟
tickets--; // 修改票数
cout << td->name << " get a ticket, "
<< "remaining tickets: " << tickets << endl;
}
这段看似简单的代码在多线程环境下会引发多种问题:
- 检查-修改的非原子性:判断
tickets>0和实际tickets--不是原子操作 - 线程调度的不确定性:线程可能在任意步骤被中断
- 数据竞争的幽灵:多个线程可能同时认为自己抢到了最后一张票
一个令人不安的实验结果
当我运行这个票务程序时,观察到了这些异常现象:
- 票数超卖:剩余票数显示为负数
- 数据不一致:控制台输出的剩余票数出现跳变
- 结果不可复现:每次运行得到的最终结果都不相同
这些现象正是并发编程中典型的**竞态条件(Race Condition)**症状,它们暴露了多线程环境下共享数据访问的根本问题:非受控的并发访问会导致不确定的行为和数据损坏。
结果展示:
...
thread-6 get a ticket, remaining tickets: 3
thread-8 get a ticket, remaining tickets: 3
thread-3 get a ticket, remaining tickets: 3
# 结果出现跳变
thread-2 get a ticket, remaining tickets: 0
thread-1 get a ticket, remaining tickets: 0
thread-9 get a ticket, remaining tickets: 0
thread-5 get a ticket, remaining tickets: 0
# 票数超卖(票数变为负数)
thread-7 get a ticket, remaining tickets: -1
thread-10 get a ticket, remaining tickets: -2
thread-4 get a ticket, remaining tickets: -3
thread-6 get a ticket, remaining tickets: -5
thread-8 get a ticket, remaining tickets: -5
问题本质的思考
为什么单线程下运行良好的代码,在多线程环境下就会出现各种异常?核心原因在于:
- 操作的非原子性:高级语言的简单语句可能对应多条机器指令
- 内存可见性问题:线程可能看到过期的数据副本
- 编译器/处理器的优化:指令重排等优化可能改变程序语义
这些底层细节在单线程环境下被完美隐藏,但在多线程环境下却成为必须面对的挑战。我们需要一种机制来规范线程对共享资源的访问,这就是我们接下来要探讨的**互斥锁(Mutex)**解决方案。
PTHREAD_MUTEX_INITIALIZER 静态初始化方式
基本语法格式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
核心特性
- 编译时初始化:在程序加载时由编译器自动完成初始化
- 零成本抽象:不产生运行时初始化开销
- 默认属性:使用所有属性为默认值的互斥锁
- 类型:普通锁(PTHREAD_MUTEX_NORMAL)
- 进程共享:仅限同一进程内(PTHREAD_PROCESS_PRIVATE)
- 不具递归性
适用场景
全局/静态互斥锁:在文件作用域或static存储期变量
// 全局范围的静态初始化 static pthread_mutex_t global_mutex = PTHREAD_MUTEX_INITIALIZER; void func() { // 函数内的静态变量 static pthread_mutex_t local_static_mutex = PTHREAD_MUTEX_INITIALIZER; }简单用例:不需要特殊属性的快速初始化
单次初始化:生命周期与程序相同的锁
典型使用示例
#include <pthread.h>
#include <stdio.h>
// 静态初始化全局互斥锁
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* increment(void* arg) {
for (int i = 0; i < 10000; ++i) {
pthread_mutex_lock(&counter_mutex);
shared_counter++;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %d\n", shared_counter);
return 0;
}
使用限制
不可用于堆内存:
// 错误用法! pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t)); *mutex = PTHREAD_MUTEX_INITIALIZER; // 未定义行为!不可重复初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init(&mutex, NULL); // 错误!不可跨模块依赖:不同编译单元间的初始化顺序不确定
底层实现原理
在Linux glibc中的典型实现:
- 宏展开为结构体初始化表达式
- 各字段被设置为表示”未锁定”状态的初始值
- 属性字段设为默认值
特殊注意事项
C++中的使用:
// 必须放在全局/命名空间作用域 namespace { pthread_mutex_t class_mutex = PTHREAD_MUTEX_INITIALIZER; } // 类静态成员(C++17后可用inline) class MyClass { static pthread_mutex_t s_mutex; }; pthread_mutex_t MyClass::s_mutex = PTHREAD_MUTEX_INITIALIZER;与动态销毁的配合:
// 合法但不必要的操作 pthread_mutex_destroy(&mutex); // 之后不能再使用该mutex
静态初始化的设计哲学
- RAII原则:利用语言特性自动管理资源
- 零开销原则:不引入不必要的运行时成本
- 简单性原则:为常见场景提供最简使用方式
pthread_mutex_init 函数:初始化互斥锁
函数原型
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
功能说明
pthread_mutex_init 用于动态初始化一个互斥锁。与静态初始化(PTHREAD_MUTEX_INITIALIZER)不同,它允许:
- 对互斥锁属性进行更精细的控制
- 在运行时动态创建互斥锁
- 初始化非全局/静态存储期的互斥锁
参数解析
- mutex:指向要初始化的互斥锁对象的指针
- attr:指向互斥锁属性对象的指针,通常为:
NULL:使用默认属性- 自定义属性对象:通过
pthread_mutexattr_init创建
返回值
- 成功:返回0
- 失败:返回错误编号(非零值)
基础使用示例
pthread_mutex_t mutex;
// 使用默认属性初始化
if (pthread_mutex_init(&mutex, NULL) != 0) {
perror("Mutex initialization failed");
exit(EXIT_FAILURE);
}
带属性的初始化示例
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
// 初始化属性对象
pthread_mutexattr_init(&attr);
// 设置互斥锁类型(如设置为递归锁)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 使用自定义属性初始化互斥锁
if (pthread_mutex_init(&mutex, &attr) != 0) {
perror("Mutex initialization failed");
exit(EXIT_FAILURE);
}
// 销毁属性对象(初始化后不再需要)
pthread_mutexattr_destroy(&attr);
注意事项
- 一对一原则:每个
pthread_mutex_init必须对应一个pthread_mutex_destroy - 避免重复初始化:不要对已初始化的互斥锁再次初始化
- 作用域管理:确保互斥锁在需要时已初始化,在不再使用时销毁
- 错误检查:总是检查返回值以确保初始化成功
典型应用场景
- 动态创建的互斥锁(如堆分配的互斥锁)
- 需要非默认属性的互斥锁(如递归锁、进程共享锁等)
- C++类中封装的互斥锁成员变量
与静态初始化的对比
| 特性 | pthread_mutex_init | PTHREAD_MUTEX_INITIALIZER |
|---|---|---|
| 初始化时机 | 运行时动态初始化 | 编译时静态初始化 |
| 属性配置 | 支持自定义属性 | 仅默认属性 |
| 适用对象 | 任意存储期的对象 | 仅全局/静态对象 |
| 错误检查 | 可检查返回值 | 无错误检查 |
配套销毁函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
应在互斥锁不再使用时调用,释放相关资源。注意:
- 确保没有线程持有或等待该锁
- 销毁后不应再使用该互斥锁
- 栈上的互斥锁在销毁前不能离开作用域
pthread_mutex_destroy 函数:销毁互斥锁
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能说明
pthread_mutex_destroy 用于销毁一个互斥锁,释放其占用的资源。
参数解析
- mutex:指向要销毁的互斥锁对象的指针
返回值
- 成功:返回0
- 失败:返回错误编号(非零值)
该函数较为简单,故不做过多赘述。
pthread_mutex_lock 函数:加锁
函数原型
int pthread_mutex_lock(pthread_mutex_t *mutex);
核心功能
- 阻塞式获取锁:如果互斥锁已被其他线程持有,调用线程将进入阻塞状态
- 原子性保证:确保锁的获取操作是原子的,不会出现竞争条件
- 临界区保护:成功获取锁后,线程可以安全执行临界区代码
参数说明
mutex:指向已初始化的互斥锁对象的指针
返回值
- 成功:返回0
- 失败:返回错误编号(非零值),常见错误:
EINVAL:互斥锁未初始化EDEADLK:检测到死锁(某些实现)
基础使用模式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void critical_section() {
// 进入临界区前加锁
int ret = pthread_mutex_lock(&mutex);
if (ret != 0) {
// 错误处理
return;
}
/* 临界区代码 */
// 离开临界区后解锁
pthread_mutex_unlock(&mutex);
}
底层工作原理
- 快速路径(无竞争时):
- 通过原子操作尝试获取锁
- 成功则立即返回
- 慢速路径(有竞争时):
- 线程进入等待队列
- 内核调度器将线程置为睡眠状态
- 当锁释放时唤醒等待线程
重要特性说明
线程阻塞行为:
- 默认情况下会使调用线程进入睡眠状态
- 不消耗CPU资源等待
- 被唤醒后会自动重新尝试获取锁
锁的归属:
- 锁与获取它的线程关联(某些类型)
- 同一线程重复加锁可能导致死锁(普通锁)
内存屏障作用:
- 包含隐式的内存屏障(memory barrier)
- 保证临界区内的内存操作不会被重排序到锁外部
错误处理
int result = pthread_mutex_lock(&mutex);
if (result != 0) {
switch(result) {
case EINVAL:
fprintf(stderr, "Mutex not initialized\n");
break;
case EDEADLK:
fprintf(stderr, "Deadlock detected\n");
break;
default:
fprintf(stderr, "Lock failed with error %d\n", result);
}
// 适当的错误恢复处理
return -1;
}
性能考量
临界区长度:
- 应保持临界区尽可能短
- 只保护真正需要同步的操作
锁竞争:
// 不良实践:在临界区内执行耗时操作 pthread_mutex_lock(&mutex); process_large_file(); // 长时间操作 pthread_mutex_unlock(&mutex); // 改进方案 pthread_mutex_lock(&mutex); int value = shared_value; // 快速获取值 pthread_mutex_unlock(&mutex); process_value(value); // 耗时操作放在锁外
递归锁特殊行为
当互斥锁被初始化为递归锁(PTHREAD_MUTEX_RECURSIVE)时:
同一线程可以多次加锁
必须有相同次数的解锁操作
示例:
pthread_mutex_lock(&mutex); // 第一次加锁 pthread_mutex_lock(&mutex); // 第二次加锁(仅递归锁允许) /* 临界区代码 */ pthread_mutex_unlock(&mutex); // 第一次解锁 pthread_mutex_unlock(&mutex); // 第二次解锁
系统级影响
- 上下文切换:可能导致线程上下文切换开销
- 调度延迟:高竞争时可能增加线程调度延迟
- 优先级反转:可能引发优先级反转问题(需配合优先级继承机制)
pthread_mutex_unlock 函数:解锁
函数原型
int pthread_mutex_unlock(pthread_mutex_t *mutex);
核心功能
- 释放锁:允许其他线程获取互斥锁
- 唤醒等待线程:如果有线程在等待该锁,唤醒其中一个
- 线程调度:被唤醒的线程可能立即运行
参数说明
mutex:指向已初始化的互斥锁对象的指针
返回值
- 成功:返回0
- 失败:返回错误编号(非零值),常见错误:
EINVAL:互斥锁未初始化EPERM:线程未持有该锁
与 pthread_mutex_lock 配套使用,不做过多赘述。
pthread_mutex_trylock 函数:非阻塞加锁
函数原型
int pthread_mutex_trylock(pthread_mutex_t *mutex);
核心特性
- 非阻塞尝试:立即返回结果,不会使线程进入等待状态
- 快速失败:当锁不可用时直接返回错误而非阻塞
- 轻量级检查:比pthread_mutex_lock有更低的性能开销
参数说明
mutex:指向已初始化的互斥锁对象的指针
返回值
- 成功获取锁:返回0
- 锁已被占用:返回EBUSY(某些系统返回EAGAIN)
- 其他错误:返回对应错误码(如EINVAL表示未初始化)
典型使用场景
- 避免死锁:在已持有某锁时尝试获取其他锁
- 优化性能:非关键路径的优化尝试
- 实时系统:不能接受阻塞的实时应用
基础使用示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void non_blocking_access() {
int ret = pthread_mutex_trylock(&mutex);
if (ret == 0) {
/* 成功获取锁,执行临界区代码 */
pthread_mutex_unlock(&mutex);
}
else if (ret == EBUSY) {
/* 锁被占用,执行替代逻辑 */
printf("Resource busy, executing fallback\n");
}
else {
/* 处理其他错误 */
perror("trylock failed");
}
}
与lock的对比分析
| 特性 | trylock | lock |
|---|---|---|
| 阻塞行为 | 永不阻塞 | 可能阻塞 |
| 返回值 | 成功/忙/错误 | 成功/错误 |
| 性能开销 | 较低 | 较高(可能涉及上下文切换) |
| 适用场景 | 非必须成功的访问 | 必须成功的访问 |
互斥锁的应用
抢票问题
下面我们针对前面那个抢票的例子使用互斥锁避免临界区被冲突访问:
情景再现:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <iostream>
using namespace std; // 使用标准命名空间
const int thread_num = 10; // 定义线程数量为10
int tickets = 1000; // 共享变量:总票数1000张
// 线程数据结构体,用于传递线程信息
struct thread_data {
pthread_t tid; // 线程ID
string name; // 线程名称
};
// 线程执行函数
void* thread_routine(void* data) {
// 将void*参数转换为thread_data*类型
thread_data* td = static_cast<thread_data*>(data);
// 获取当前线程ID
td->tid = pthread_self();
// 循环抢票
while (true) {
// 检查是否还有票
if (tickets > 0) {
usleep(1000); // 模拟业务处理延迟(1毫秒)
tickets--; // 票数减1(临界区操作)
// 输出抢票信息
cout << td->name << " get a ticket, "
<< "remaining tickets: " << tickets << endl;
} else {
break; // 没有票了,退出循环
}
}
delete td; // 释放线程数据内存
return nullptr; // 线程返回空指针
}
int main() {
vector<pthread_t> tids; // 存储线程ID
// 创建10个线程
for (int i = 1; i <= thread_num; ++i) {
string thread_name = "thread-" + to_string(i); // 生成线程名称
pthread_t tid; // 线程ID变量
// 创建线程数据对象
thread_data* td = new thread_data{
name: thread_name // 初始化线程名称
};
// 创建线程
pthread_create(&tid, nullptr, thread_routine, td);
tids.push_back(tid); // 将线程ID加入向量
}
// 等待所有线程结束
for (auto tid : tids) {
pthread_join(tid, nullptr);
}
return 0; // 主线程退出
}
接下来我们通过加锁来解决问题:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <iostream>
#include <chrono>
using namespace std; // 使用标准命名空间
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化全局互斥锁
const int thread_num = 10; // 定义线程数量为10
int tickets = 1000; // 共享变量:总票数1000张
// 线程数据结构体,用于传递线程信息
struct thread_data
{
pthread_t tid; // 线程ID
string name; // 线程名称
};
// 计时器
class Timer
{
private:
std::chrono::time_point<std::chrono::high_resolution_clock> start_time;
public:
Timer()
{
start();
}
void start()
{
start_time = std::chrono::high_resolution_clock::now();
}
~Timer()
{
std::cout << elapsed() << endl;
}
// 返回经过的毫秒数
double elapsed() const
{
auto end_time = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double, std::milli>(end_time - start_time).count();
}
// 返回经过的秒数
double elapsedSeconds() const
{
return elapsed() / 1000.0;
}
};
// 线程执行函数
void *thread_routine(void *data)
{
// 将void*参数转换为thread_data*类型
thread_data *td = static_cast<thread_data *>(data);
// 获取当前线程ID
td->tid = pthread_self();
// 循环抢票
while (true)
{
// 检查是否还有票
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000); // 模拟业务处理延迟(1毫秒)
tickets--; // 票数减1(临界区操作)
// 输出抢票信息
cout << td->name << " get a ticket, "
<< "remaining tickets: " << tickets << endl;
}
else
{
pthread_mutex_unlock(&mutex);
break; // 没有票了,退出循环
}
pthread_mutex_unlock(&mutex);
}
delete td; // 释放线程数据内存
return nullptr; // 线程返回空指针
}
int main()
{
vector<pthread_t> tids; // 存储线程ID
// 创建10个线程
for (int i = 1; i <= thread_num; ++i)
{
string thread_name = "thread-" + to_string(i); // 生成线程名称
pthread_t tid; // 线程ID变量
// 创建线程数据对象
thread_data *td = new thread_data{
name : thread_name // 初始化线程名称
};
// 创建线程
pthread_create(&tid, nullptr, thread_routine, td);
tids.push_back(tid); // 将线程ID加入向量
}
// 等待所有线程结束
{
Timer timer;
for (auto tid : tids)
{
pthread_join(tid, nullptr);
}
}
return 0; // 主线程退出
}
我们会发现程序运行没有任何问题:
thread-1 get a ticket, remaining tickets: 3
thread-1 get a ticket, remaining tickets: 2
thread-1 get a ticket, remaining tickets: 1
thread-1 get a ticket, remaining tickets: 0
1084.92
若不加锁,结果如下:
thread-8thread-2 get a ticket, remaining tickets: -4
thread-5 get a ticket, remaining tickets: -6
thread-3 get a ticket, remaining tickets: -7
thread-6 get a ticket, remaining tickets: -8
thread-9 get a ticket, remaining tickets: -8
get a ticket, remaining tickets: -8
thread-10 get a ticket, remaining tickets: -9
110.501
可以发现,通过加互斥锁,结果变得正确,不过代价就是会消耗更多的时间,这也正是为什么在保证数据安全的前提下,临界区越小越好的原因
互斥锁的封装
接下来,我们尝试对 Linux 的锁进行封装:
#ifndef _LOCK_H_
#define _LOCK_H_ 1
#include <pthread.h> // POSIX线程库头文件
#include <exception> // 标准异常处理头文件
#include <iostream> // 标准输入输出头文件
/**
* @class Mutex
* @brief 封装POSIX互斥锁的线程安全互斥量类
*
* 该类提供了基本的互斥锁功能,包括加锁、解锁和尝试加锁操作。
* 使用RAII模式管理锁资源,确保锁的正确初始化和释放。
*/
class Mutex
{
public:
/**
* @brief 构造函数,初始化互斥锁
* @throw std::exception 如果互斥锁初始化失败
*/
Mutex()
{
// 初始化互斥锁,使用默认属性(nullptr)
if (pthread_mutex_init(&mutex_, nullptr) != 0)
{
throw std::exception(); // 初始化失败抛出异常
}
}
/**
* @brief 析构函数,销毁互斥锁
*/
~Mutex()
{
pthread_mutex_destroy(&mutex_); // 销毁互斥锁
}
// 禁用拷贝构造函数和赋值运算符
Mutex(const Mutex&) = delete; // 拷贝构造禁用
Mutex& operator=(const Mutex&) = delete; // 赋值操作禁用
/**
* @brief 加锁操作(阻塞式)
*
* 如果锁已被其他线程持有,调用线程将被阻塞直到获得锁。
*/
void lock()
{
pthread_mutex_lock(&mutex_); // POSIX加锁操作
}
/**
* @brief 尝试加锁(非阻塞式)
* @return bool 加锁成功返回true,失败返回false
*/
bool try_lock()
{
return pthread_mutex_trylock(&mutex_) == 0; // 尝试加锁,不阻塞
}
/**
* @brief 解锁操作
*/
void unlock()
{
pthread_mutex_unlock(&mutex_); // POSIX解锁操作
}
/**
* @brief 获取原生互斥锁句柄
* @return pthread_mutex_t* 指向底层POSIX互斥锁的指针
*
* 用于需要与POSIX线程API直接交互的场景。
*/
pthread_mutex_t* native_handle()
{
return &mutex_; // 返回底层互斥锁指针
}
private:
pthread_mutex_t mutex_; // POSIX互斥锁对象
};
/**
* @class LockGuard
* @brief 互斥锁守卫类,实现RAII风格的锁管理
*
* 在构造时自动加锁,析构时自动解锁,确保锁的释放,
* 即使在异常发生时也能保证锁被正确释放。
*/
class LockGuard
{
public:
/**
* @brief 构造函数,自动加锁
* @param mutex 要管理的互斥锁引用
*
* @note explicit关键字防止隐式转换
*/
explicit LockGuard(Mutex &mutex) : mutex_(mutex)
{
mutex_.lock(); // 构造时自动加锁
}
/**
* @brief 析构函数,自动解锁
*/
~LockGuard()
{
mutex_.unlock(); // 析构时自动解锁
}
// 禁用拷贝构造函数和赋值运算符
LockGuard(const LockGuard&) = delete; // 拷贝构造禁用
LockGuard& operator=(const LockGuard&) = delete; // 赋值操作禁用
private:
Mutex &mutex_; // 管理的互斥锁引用
};
#endif // _LOCK_H_
我们将这个基于 RAII 自动化管理的锁运用到我们的抢票程序中:
// 循环抢票
while (true)
{
// 检查是否还有票
// pthread_mutex_lock(&mutex);
LockGuard lg(*td->mutex);
if (tickets > 0)
{
usleep(1000); // 模拟业务处理延迟(1毫秒)
tickets--; // 票数减1(临界区操作)
// 输出抢票信息
cout << td->name << " get a ticket, "
<< "remaining tickets: " << tickets << endl;
}
else
{
// pthread_mutex_unlock(&mutex);
break; // 没有票了,退出循环
}
// pthread_mutex_unlock(&mutex);
}
可以看到,RAII 自动化管理只需要我们再循环开始处初始化变量即可,只要退出当前循环就一定会解锁,原先我们若在循环内部进行加锁和解锁操作,我们还需要关心 else 执行流中 break,因此需要额外在 else 分支中加上解锁操作。合理地利用 C++ 的特性来简化代码和增加代码健壮性是一个程序员的基本素养
结果如下:
thread-2 get a ticket, remaining tickets: 3
thread-2 get a ticket, remaining tickets: 2
thread-2 get a ticket, remaining tickets: 1
thread-2 get a ticket, remaining tickets: 0
1086.37