Bootstrap

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() {
  EventQueue eq;
  std::thread prod(producer, std::ref(eq), 100);
  std::thread cons(consumer, std::ref(eq), 100);

  prod.join();
  eq.stop_consuming();
  cons.join();

  return 0;
}

2. 代码概要说明

  • 使用 std::lock() 在持有锁的情况下安全地获取 std::shared_ptr,避免竞态条件。
  • stop_consuming() 中使用 std::atomic<bool> 类型的 stop_ 变量来安全地停止消费者线程。
  • 生产者线程使用 push_event 向事件队列添加事件,消费者线程使用 consume_events 处理事件,确保线程安全和资源管理。

3. 消费者线程设计(安全与非安全)

添加了 #ifdef UN_SAFE 条件编译指令,用来区分安全和不安全的版本。在不安全版本中,消费者线程在持有互斥锁的情况下,直接调用 event_item.second.lock() 来获取 shared_ptr,并将其作为参数传递给回调函数 callback,而没有检查 shared_ptr 是否为 nullptr

这种不安全的实现方式可能导致以下问题:

  • 如果在事件处理期间,与该事件关联的对象被销毁,event_item.second.lock() 将返回一个空的 shared_ptr,而后续代码未对此进行检查,可能导致空指针访问或者悬空引用的情况。
  • 如果 callback 函数假设 shared_ptr 总是有效,那么在实际应用中可能会导致未定义的行为或者崩溃。

这种不安全的实现示例强调了在多线程编程中正确处理共享资源的重要性,特别是在处理智能指针时需要格外小心,以避免悬空引用和内存访问错误。

消费者线程使用了更安全的方式来处理事件队列中的 std::weak_ptr

if (auto shared_ptr = event_item.second.lock()) {
    if (callback)
        callback(event_item.first, shared_ptr);
}

这里使用了 lock() 函数在持有互斥锁的情况下获取 shared_ptr,从而避免了竞态条件。在处理事件时,只有在确保资源仍然有效的情况下才会调用回调函数。

4. 跨模块传递 std::weak_ptr 的问题

在不同模块间传递 std::shared_ptrstd::weak_ptr 时,确保它们引用的是相同的控制块是关键。可以通过以下方法解决:

  • 在模块间共享相同的 std::shared_ptr 实例,或者显式传递以确保控制块的一致性。

5. 为什么使用 std::weak_ptr 而不是 std::shared_ptr

在事件队列中选择使用 std::weak_ptr 的理由主要有:

  • 避免循环引用:如果事件类型内部已经使用了 std::shared_ptr,在事件队列中继续使用 std::shared_ptr 可能导致循环引用,影响资源的释放。
  • 延迟强引用:消费者处理事件时可以根据需要通过 lock() 升级为 std::shared_ptr,避免了不必要的强引用创建和维持。

6. 执行结果

不安全代码出现了错误执行结果:
在这里插入图片描述

;