在多线程程序中,常见的同步问题之一是共享资源的安全访问。传统做法往往是显式地使用 std::mutex 并在访问完成后手动解锁,容易出现忘记解锁、死锁等错误。C++ 的 RAII(Resource Acquisition Is Initialization)模式可以帮助我们以更安全、简洁的方式管理共享资源。下面演示一种基于 RAII 的多线程共享资源管理方案,并给出完整可编译的示例代码。
1. 设计思路
- 封装互斥量:创建一个
MutexGuard类,在构造函数中锁定std::mutex,在析构函数中解锁。这样只要对象生命周期结束,锁就会自动释放,避免手动解锁的遗漏。 - 共享资源包装:将共享数据放在一个
ThreadSafeContainer类中,该类内部持有MutexGuard并提供对数据的访问接口。所有对共享资源的访问都必须通过该类的接口完成,保证了线程安全。 - 避免死锁:在同一个
ThreadSafeContainer内部,只使用单一互斥量,且不在锁定状态下调用其他锁,天然避免了死锁。
2. 代码实现
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>
#include <random>
/**
* RAII-style mutex guard
*/
class MutexGuard {
public:
explicit MutexGuard(std::mutex& mtx) : mtx_(mtx) {
mtx_.lock();
}
~MutexGuard() {
mtx_.unlock();
}
private:
std::mutex& mtx_;
};
/**
* Thread-safe container for an integer vector
*/
template<typename T>
class ThreadSafeContainer {
public:
ThreadSafeContainer() = default;
// 禁止拷贝与移动
ThreadSafeContainer(const ThreadSafeContainer&) = delete;
ThreadSafeContainer& operator=(const ThreadSafeContainer&) = delete;
// 插入元素
void push_back(const T& value) {
MutexGuard guard(mtx_);
data_.push_back(value);
}
// 读取元素,返回拷贝
T at(size_t index) const {
MutexGuard guard(mtx_);
if (index >= data_.size()) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}
// 获取容器大小
size_t size() const {
MutexGuard guard(mtx_);
return data_.size();
}
private:
mutable std::mutex mtx_;
std::vector <T> data_;
};
/**
* 生产者线程:向容器中不断添加随机整数
*/
void producer(ThreadSafeContainer <int>& container, int thread_id) {
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution <int> dist(1, 100);
for (int i = 0; i < 100; ++i) {
int val = dist(rng);
container.push_back(val);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::cout << "Producer " << thread_id << " finished.\n";
}
/**
* 消费者线程:尝试读取容器中的元素
*/
void consumer(const ThreadSafeContainer <int>& container, int thread_id) {
for (int i = 0; i < 50; ++i) {
try {
size_t sz = container.size();
if (sz > 0) {
int val = container.at(0);
std::cout << "Consumer " << thread_id << " read value: " << val << "\n";
}
} catch (const std::exception& e) {
std::cerr << "Consumer " << thread_id << " error: " << e.what() << "\n";
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
std::cout << "Consumer " << thread_id << " finished.\n";
}
int main() {
ThreadSafeContainer <int> container;
// 创建生产者和消费者线程
std::vector<std::thread> producers;
std::vector<std::thread> consumers;
for (int i = 0; i < 3; ++i) {
producers.emplace_back(producer, std::ref(container), i + 1);
}
for (int i = 0; i < 2; ++i) {
consumers.emplace_back(consumer, std::cref(container), i + 1);
}
// 等待所有线程完成
for (auto& t : producers) t.join();
for (auto& t : consumers) t.join();
std::cout << "Final container size: " << container.size() << "\n";
return 0;
}
3. 关键点剖析
- MutexGuard:构造函数锁定互斥量,析构函数解锁,确保异常安全。因为
std::mutex的lock()/unlock()是不可抛异常的,故不需要额外的错误处理。 - ThreadSafeContainer:所有对
data_的访问都在MutexGuard的保护下进行。mutable关键字允许在const成员函数中修改互斥量。 - 异常安全:在
at()中若越界会抛出std::out_of_range,但锁已经在MutexGuard的析构中安全释放。 - 性能考虑:如果并发量极高,可以进一步使用
std::shared_mutex让读操作共享锁,写操作独占锁。但此处为了演示简洁,使用的是普通互斥量。
4. 扩展思路
- 读写锁:对读多写少的场景使用
std::shared_mutex,实现shared_lock与unique_lock的组合。 - 事务化操作:在
ThreadSafeContainer内实现批量插入、删除等操作,保证原子性。 - 与条件变量配合:当消费者需要等待生产者生产一定数量后再继续,可以加入
std::condition_variable。
总结
通过 RAII 对互斥量进行封装,C++ 中的多线程共享资源管理可以变得异常简单、安全。只需将访问逻辑包装进类中,所有线程即可安全共享数据,避免手动锁/解锁带来的错误。希望本文能帮助你在实际项目中快速实现线程安全的数据结构。