文章目录
0. 概要
[前置阅读] C++编程:使用std::weak_ptr监控std::shared_ptr
为了展示竞态条件的可能性,并且验证更安全的代码是如何避免这种竞态条件的,可以创建一个简单的多线程测试程序。
我们将使用一个生产者-消费者模型,其中生产者向队列中添加带有 std::shared_ptr
的事件,而消费者则从队列中取出事件并处理它们。
我们将实现两个版本:
- 不安全版本:不检查
std::shared_ptr
是否有效 - 安全的版本:消费者线程在持有锁的情况下检查
std::shared_ptr
的有效性。
1. 实例代码
以下是一个使用 C++11 标准库编写的测试程序示例:
#include <atomic>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
struct Event {
int id;
};
class EventQueue {
public:
void push_event(const Event &e, std::shared_ptr<void> ptr) {
std::unique_lock<std::mutex> lck(mtx_);
events_.push({e, std::weak_ptr<void>(ptr)});
cv_.notify_one();
}
void consume_events(
std::function<void(Event, std::shared_ptr<void>)> callback) {
std::unique_lock<std::mutex> lck(mtx_);
while (true) {
cv_.wait(lck, [this] { return !events_.empty() || stop_.load(); });
if (stop_.load()) {
break;
}
auto event_item = events_.front();
events_.pop();
#ifndef UN_SAFE
// 安全版本:在持有锁的情况下检查 shared_ptr 是否有效
if (const auto &shared_ptr = event_item.second.lock()) {
if (callback != nullptr) {
callback(event_item.first, shared_ptr);
}
}
#else
// 不安全版本:不检查 shared_ptr 是否有效
if (callback != nullptr) {
callback(event_item.first, event_item.second.lock());
}
#endif
}
}
void stop_consuming() {
stop_.store(true);
cv_.notify_all();
}
private:
std::queue<std::pair<Event, std::weak_ptr<void>>> events_;
std::mutex mtx_;
std::condition_variable cv_;
std::atomic<bool> stop_{false};
};
void producer(EventQueue &eq, int num_events) {
for (int i = 0; i < num_events; ++i) {
Event e{i};
std::shared_ptr<void> ptr(new int(i));
eq.push_event(e, ptr);
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟耗时操作
}
}
void consumer(EventQueue &eq, int num_events) {
eq.consume_events([](Event e, std::shared_ptr<void> ptr) {
std::cout << "Event ID: " << e.id << ", Pointer valid: " << std::boolalpha
<< (ptr.get() != nullptr) << std::endl;
});
}
int main() {
// 不安全的代码设计,一般跑不了1000次循环测试。
static uint32_t count = 1;
while (count < 1000) {
EventQueue eq;
std::thread prod(producer, std::ref(eq), 10);
std::thread cons(consumer, std::ref(eq), 10);
prod.join();
eq.stop_consuming();
cons.join();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "\n----- "
<< "count:" << count++ << " -----\n";
}
return 0;
}
2. #ifndef UN_SAFE
分支下智能指针失效
2.1 以上代码执行结果
- 测试
不安全
代码
使用如下命令编译和执行,在第55次的时候出现了错误
g++ -o unsafe test.cpp -lpthread -DUN_SAFE -O2
./unsafe
- 测试
安全
代码
g++ -o unsafe test.cpp -lpthread -O2
./unsafe
在运行1000多次后,一次错误均未发生。
2.2 #ifndef UN_SAFE
分支失效原因分析
在 #ifndef UN_SAFE
分支下,代码从队列中取出 std::weak_ptr<void>
后立即释放了锁,随后尝试锁定 std::weak_ptr
为 std::shared_ptr<void>
。然而,在释放锁和锁定 std::weak_ptr
之间的间隙,另一线程可能销毁了 std::shared_ptr<void>
,导致智能指针失效。
具体看这段代码:
auto event_item = events_.front();
events_.pop();
// 锁在此处释放
// ...
if (callback != nullptr) {
callback(event_item.first, event_item.second.lock());
}
在 #ifndef UN_SAFE
处,锁在 events_.pop();
后释放,而 event_item.second.lock();
在无锁保护的情况下执行,这可能使得 std::weak_ptr
锁定失败,返回 nullptr
。
3. 预防智能指针失效策略
3.1. 线程安全机制与智能指针
- 智能指针与互斥锁的协同工作:利用
std::lock_guard
确保在持有互斥锁的情况下安全地操作std::shared_ptr
,有效防止竞态条件的发生。 - 原子变量控制线程终止:在
stop_consuming()
函数中,使用std::atomic<bool>
类型的stop_
变量来安全地控制消费者线程的终止,保证线程间通信的原子性。 - 生产者-消费者模式下的线程安全:生产者线程通过
push_event
向事件队列添加事件,消费者线程则使用consume_events
处理事件,确保所有操作在多线程环境下既安全又高效。
3.2. 安全与非安全消费者线程设计对比
-
测试代码的核心部分:
#ifndef UN_SAFE // 安全版本:在持有锁的情况下检查 shared_ptr 是否有效 if (const auto &shared_ptr = event_item.second.lock()) { if (callback != nullptr) { callback(event_item.first, shared_ptr); } } #else // 不安全版本:不检查 shared_ptr 是否有效 if (callback != nullptr) { callback(event_item.first, event_item.second.lock()); } #endif
-
非安全实现的问题:当
#ifdef UN_SAFE
条件下,消费者线程在持有互斥锁时直接调用event_item.second.lock()
来获取shared_ptr
,并将其作为参数传递给回调函数callback
,但缺乏对shared_ptr
是否为nullptr
的检查。这可能导致在对象已被销毁的情况下访问空shared_ptr
,引发未定义行为或程序崩溃。 -
安全实现的策略:通过先检查
shared_ptr
的有效性再调用回调函数的方法,确保资源在被访问前仍然有效。具体做法是:if (const auto &shared_ptr = event_item.second.lock()) { if (callback != nullptr) { callback(event_item.first, shared_ptr); } }
这样一来,即使在事件处理期间与该事件关联的对象被销毁,也不会导致空指针访问或悬空引用的情况发生。
3.3. 跨模块传递std::weak_ptr
的一致性与循环引用预防
- 跨模块传递的挑战与解决方案:在不同模块间传递
std::weak_ptr
时,重要的是确保它们引用的是相同的控制块。为了解决这个问题,可以共享同一个std::shared_ptr
实例,或者显式地传递以确保控制块的一致性。 - 选择
std::weak_ptr
而非std::shared_ptr
的原因:- 避免循环引用:如果事件类型内部已经使用了
std::shared_ptr
,那么在事件队列中继续使用std::shared_ptr
可能会形成循环引用,阻碍资源的正常释放。
关于weak_ptr解决循环引用请查看 C++编程:使用 std::weak_ptr 解决 std::shared_ptr 循环引用导致的内存泄漏 - 延迟强引用提升:消费者线程在处理事件时可以根据需要通过
lock()
方法升级std::weak_ptr
为std::shared_ptr
,这样避免了不必要的强引用创建和维持,提高了性能和资源管理效率。
- 避免循环引用:如果事件类型内部已经使用了