C++编程:使用std::weak_ptr监控std::shared_ptr解决多线程竞态实例(智能指针失效)

0. 概要

[前置阅读] C++编程:使用std::weak_ptr监控std::shared_ptr

为了展示竞态条件的可能性,并且验证更安全的代码是如何避免这种竞态条件的,可以创建一个简单的多线程测试程序。
我们将使用一个生产者-消费者模型,其中生产者向队列中添加带有 std::shared_ptr 的事件,而消费者则从队列中取出事件并处理它们。

我们将实现两个版本:

  1. 不安全版本:不检查 std::shared_ptr 是否有效
  2. 安全的版本:消费者线程在持有锁的情况下检查 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_ptrstd::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_ptrstd::shared_ptr,这样避免了不必要的强引用创建和维持,提高了性能和资源管理效率。
  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值