【Linux】从互斥原理到C++ RAII封装实践
📢博客主页:https://blog.csdn.net/2301_779549673
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
- 📢前言
- 🏳️🌈一、从场景看互斥:为什么需要锁?
- 🏳️🌈二、互斥锁核心概念图解
- 2.1 临界区与非临界区
- 2. 2 进程线程间的互斥相关背景概念
- 🏳️🌈三、Linux互斥锁原理剖析
- 3.2 初始化互斥量
- 3.3 销毁互斥量
- 3.3 pthread_mutex 底层实现
- 🏳️🌈四、C++ RAII封装实战
- 4.1 基础互斥类(Mutex)
- 4.2 守卫锁(LockGuard)
- 🏳️🌈五、完整代码
- 5.1 Mutex.hpp
- 5.2 Mutex.cc
- 5.3 Makefile
- 👥总结
📢前言
紧接上回的线程C++封装
,这回笔者着重介绍一下互斥的原理和其必要性,并手把手使用C++封装一个RAII模型。
还有一点,笔者之后的封装都会使用之前博客中封装好的容器,需要的可以去仓库或者前面的博客中自取。
RAII
的核心思想是将资源的获取和初始化放在对象的构造函数中进行,而资源的释放放在对象的析构函数中进行。当对象被创建时,其构造函数会自动执行,从而完成资源的获取;当对象的生命周期结束时,其析构函数会被自动调用,从而完成资源的释放。这样,资源的生命周期就与对象的生命周期绑定在一起,利用 C++ 等语言的对象自动销毁机制来确保资源的正确释放。
🏳️🌈一、从场景看互斥:为什么需要锁?
假设你的银行账户余额是1000元,同时有两个线程执行转账操作:
- 线程A:存入200元 → balance += 200
- 线程B:取出300元 → balance -= 300
无锁情况下可能的执行顺序:
线程A读取balance(1000) → 线程B读取balance(1000) →
线程A写入1200 → 线程B写入700
最终结果:700元(正确应为900元)
🏳️🌈二、互斥锁核心概念图解
2.1 临界区与非临界区
void* thread_func(void* arg) {
// 非临界区(可并发执行)
prepare_data();
// 临界区(需互斥访问)
pthread_mutex_lock(&mtx);
update_shared_resource();
pthread_mutex_unlock(&mtx);
// 非临界区(可并发执行)
post_process();
}
关键特征:
🔵 非临界区:允许多线程并行(如图中绿色区域)
🔴 临界区:同一时刻仅一个线程执行(红色区域)
2. 2 进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
🏳️🌈三、Linux互斥锁原理剖析
核心操作流程:
sequenceDiagram
participant 线程A
participant 互斥锁
participant 内核
线程A->>互斥锁: pthread_mutex_lock()
alt 锁空闲
互斥锁-->>线程A: 立即获得锁
else 锁被占
线程A->>内核: 进入休眠队列
内核-->>线程A: 唤醒并获取锁
end
线程A->>临界区: 执行操作
线程A->>互斥锁: pthread_mutex_unlock()
3.2 初始化互斥量
方法一:静态分布
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分布
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:要初始化的互斥量
attr:NULL
3.3 销毁互斥量
销毁互斥量需要注意:
- 使用 PTHREAD_MUTEXINITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤ pthread_mutex_lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
3.3 pthread_mutex 底层实现
// 锁结构(简化为x86实现)
struct pthread_mutex {
int __lock; // 锁状态标识
int __count; // 递归锁计数器
int __owner; // 持有者线程ID
int __kind; // 锁类型标识
// ... 其他字段
};
🏳️🌈四、C++ RAII封装实战
4.1 基础互斥类(Mutex)
// 互斥锁封装类(不可拷贝构造/赋值)
class Mutex
{
public:
// 禁止拷贝(保护系统锁资源)
Mutex(const Mutex&) = delete;
const Mutex& operator = (const Mutex&) = delete;
// 构造函数:初始化POSIX互斥锁
Mutex()
{
// 初始化互斥锁属性为默认值
int n = ::pthread_mutex_init(&_lock, nullptr);
(void)n; // 实际开发建议处理错误码
}
// 析构函数:销毁锁资源
~Mutex()
{
// 确保锁已处于未锁定状态
int n = ::pthread_mutex_destroy(&_lock);
(void)n; // 生产环境应检查返回值
}
// 加锁操作(阻塞直至获取锁)
void Lock()
{
// 可能返回EDEADLK(死锁检测)等错误码
int n = ::pthread_mutex_lock(&_lock);
(void)n; // 简化处理,实际建议抛异常或记录日志
}
// 解锁操作(必须由锁持有者调用)
void Unlock()
{
// 未持有锁时解锁将返回EPERM
int n = ::pthread_mutex_unlock(&_lock);
(void)n;
}
private:
pthread_mutex_t _lock; // 底层锁对象
};
4.2 守卫锁(LockGuard)
守卫锁不是新的锁类型,而是对已有锁的自动化生命周期管理工具。这种设计模式完美契合图示中"Lock-unlock"边界需要严格匹配的核心诉求
守卫锁工作流程
sequenceDiagram
participant 线程
participant 守卫锁
participant 互斥锁
线程->>守卫锁: 创建LockGuard对象
守卫锁->>互斥锁: 调用Lock()
互斥锁-->>守卫锁: 获得锁
线程->>临界区: 执行操作
线程->>守卫锁: 对象离开作用域
守卫锁->>互斥锁: 调用Unlock()
互斥锁-->>其他线程: 释放锁资源
实现
// RAII锁守卫(自动管理锁生命周期)
class LockGuard
{
public:
// 构造时加锁(必须传入已初始化的Mutex引用)
LockGuard(Mutex &mtx):_mtx(mtx)
{
_mtx.Lock(); // 进入临界区
}
// 析构时自动解锁(异常安全保证)
~LockGuard()
{
_mtx.Unlock(); // 离开作用域自动释放
}
private:
Mutex &_mtx; // 引用方式持有,避免拷贝导致未定义行为
};
🏳️🌈五、完整代码
5.1 Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h> // POSIX线程库头文件
namespace LockModule
{
// 互斥锁封装类(不可拷贝构造/赋值)
class Mutex
{
public:
// 禁止拷贝(保护系统锁资源)
Mutex(const Mutex&) = delete;
const Mutex& operator = (const Mutex&) = delete;
// 构造函数:初始化POSIX互斥锁
Mutex()
{
// 初始化互斥锁属性为默认值
int n = ::pthread_mutex_init(&_lock, nullptr);
(void)n; // 实际开发建议处理错误码
}
// 析构函数:销毁锁资源
~Mutex()
{
// 确保锁已处于未锁定状态
int n = ::pthread_mutex_destroy(&_lock);
(void)n; // 生产环境应检查返回值
}
// 加锁操作(阻塞直至获取锁)
void Lock()
{
// 可能返回EDEADLK(死锁检测)等错误码
int n = ::pthread_mutex_lock(&_lock);
(void)n; // 简化处理,实际建议抛异常或记录日志
}
// 解锁操作(必须由锁持有者调用)
void Unlock()
{
// 未持有锁时解锁将返回EPERM
int n = ::pthread_mutex_unlock(&_lock);
(void)n;
}
private:
pthread_mutex_t _lock; // 底层锁对象
};
// RAII锁守卫(自动管理锁生命周期)
class LockGuard
{
public:
// 构造时加锁(必须传入已初始化的Mutex引用)
LockGuard(Mutex &mtx):_mtx(mtx)
{
_mtx.Lock(); // 进入临界区
}
// 析构时自动解锁(异常安全保证)
~LockGuard()
{
_mtx.Unlock(); // 离开作用域自动释放
}
private:
Mutex &_mtx; // 引用方式持有,避免拷贝导致未定义行为
};
}
5.2 Mutex.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 0;
pthread_mutex_t mutex;
pthread_cond_t cond = PTHREAD_COND_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
{
printf("%s wait on cond!\n", id);
pthread_cond_wait(&cond, &mutex); //醒来的时候,会重新申请锁!!
printf("%s 被叫醒了\n", id);
}
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
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");
int cnt = 10;
while(true)
{
sleep(5);
ticket += cnt;
printf("主线程放票喽, ticket: %d\n", ticket);
pthread_cond_signal(&cond);
}
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
5.3 Makefile
bin=testMutex
cc=g++
src=$(wildcard *.cc)
obj=$(src:.cc=.o)
$(bin):$(obj)
$(cc) -o $@ $^ -lpthread
%.o:%.cc
$(cc) -c $< -std=c++17
.PHONY:clean
clean:
rm -f $(bin) $(obj)
.PHONY:test
test:
echo $(src)
echo $(obj)
👥总结
本篇博文对 从互斥原理到C++ RAII封装实践 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~