深入理解 shared_ptrweak_ptr:访问控制与线程安全

引言

在多线程编程中,合理使用智能指针(如 std::shared_ptrstd::weak_ptr)对于管理资源的生命周期和避免潜在的竞争条件至关重要。本文将通过两个版本的 main 函数示例来探讨如何利用 weak_ptr 实现对对象的安全访问,以及如何演示访问成功与失败的情况。

示例代码概览

我们将定义一个简单的类 A,它包含一个成员函数 testA。此外,我们还会创建一个子线程函数 handler01,该函数接收一个 weak_ptr<A> 参数,并尝试访问 A 对象。最后,我们将展示两个版本的 main 函数,分别对应于可以访问和无法访问 A 对象的情形。

类定义与子线程函数
 #include <iostream>
 #include <memory>
 #include <thread>
 #include <chrono>
 ​
 using namespace std;
 ​
 class A {
 public:
     void testA() {
         cout << "testA function called" << endl;
     }
 };
 ​
 // 子线程
 void handler01(weak_ptr<A> pw) {
     std::this_thread::sleep_for(std::chrono::seconds(2));
     // 使用 weak_ptr 转换为 shared_ptr 进行安全访问
     shared_ptr<A> sp = pw.lock();
     if (sp != nullptr) {
         sp->testA();
     } else {
         cout << "A对象已经析构," << endl;
     }
 }
版本一:能够访问 A 对象

在这个版本中,主线程等待的时间足够长,使得子线程有机会在 A 对象被销毁之前访问它。

 int main() {
     {
         shared_ptr<A> p(new A());
         thread t1(handler01, weak_ptr<A>(p));
 ​
         // 主线程等待足够长时间确保子线程可以访问A对象
         std::this_thread::sleep_for(std::chrono::seconds(3)); // 等待时间延长
 ​
         // 阻塞等待子线程结束
         t1.join();
     }
 ​
     return 0;
 }

输出结果

 testA function called

解释

  • 在这个版本中,主线程等待了足够长的时间(3秒),使得子线程能够在 A 对象被销毁前锁定并调用 testA 方法。

  • 输出显示子线程成功调用了 testA 方法。

版本二:无法访问 A 对象

在这个版本中,主线程等待的时间较短,导致子线程尝试访问时 A 对象已经被销毁。

 int main() {
     {
         shared_ptr<A> p(new A());
         thread t1(handler01, weak_ptr<A>(p));
 ​
         // 主线程等待一段时间后释放A对象
         std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 缩短等待时间
         p.reset(); // 释放A对象
 ​
         // 阻塞等待子线程结束
         t1.join();
     }
 ​
     return 0;
 }

输出结果

 A对象已经析构,

解释

  • 在这个版本中,主线程只等待了很短的时间(500毫秒),然后就释放了 A 对象。

  • 当子线程尝试访问 A 对象时,发现对象已被销毁,因此输出提示信息“A对象已经析构”。

weak_ptr 的详细用法

std::weak_ptr 是一种不增加引用计数的智能指针,它用于观察 std::shared_ptr 所管理的对象。weak_ptr 不拥有对象的所有权,因此不会阻止对象的销毁。这使得 weak_ptr 成为解决循环引用问题的理想选择。

基本用法
  1. 构造 weak_ptr:

    • 可以从 shared_ptr 构造 weak_ptr

    • 也可以从另一个 weak_ptr 构造新的 weak_ptr

     shared_ptr<A> sp(new A());
     weak_ptr<A> wp1(sp); // 从 shared_ptr 构造
     weak_ptr<A> wp2(wp1); // 从 weak_ptr 构造
  2. 使用 lock() 方法:

    • lock() 方法尝试将 weak_ptr 转换为 shared_ptr

    • 如果对象仍然存在,则返回一个 shared_ptr 指向该对象。

    • 如果对象已被销毁,则返回一个空的 shared_ptr

     shared_ptr<A> sp = wp.lock();
     if (sp) {
         sp->testA(); // 安全访问对象
     } else {
         cout << "对象已销毁" << endl;
     }
  3. 检查是否过期:

    • 使用 expired() 方法检查 weak_ptr 是否指向一个已销毁的对象。

     if (wp.expired()) {
         cout << "对象已销毁" << endl;
     }
  4. 重置 weak_ptr:

    • 使用 reset() 方法将 weak_ptr 设置为 nullptr

     wp.reset();
优点
  • 避免循环引用:

    • weak_ptr 不增加引用计数,因此不会导致循环引用问题。

    • 例如,在双向链表中,父节点可以使用 shared_ptr 指向子节点,而子节点可以使用 weak_ptr 指向父节点。

  • 线程安全:

    • weak_ptr 提供了一种安全的方式来访问可能已被销毁的对象。

    • 通过 lock() 方法,可以在多线程环境中安全地访问共享资源。

  • 资源管理:

    • weak_ptr 允许程序在需要时检查对象是否存在,从而避免访问已销毁的对象。

总结

通过上述两个版本的 main 函数示例,我们可以清楚地看到 weak_ptr 在处理可能已销毁的对象时的重要性。使用 lock() 方法尝试将 weak_ptr 提升为 shared_ptr 是一种安全的做法,可以避免访问已销毁的对象,从而防止程序崩溃或产生未定义行为。这种方法不仅适用于单线程环境,在多线程环境中尤为重要,因为它帮助我们有效地管理对象的生命周期,同时保证了线程安全。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐