C++编程: 使用 std::enable_shared_from_this 解决多线程悬空指针(智能指针失效)

0. 概要

C++编程:使用std::weak_ptr监控std::shared_ptr解决多线程竞态实例(智能指针失效)中我们提到了悬空指针(智能指针失效)的问题。

std::enable_shared_from_this 也可是避免在多线程环境中因对象提前销毁而导致的std::shared_ptr悬空指针访问问题。

本文将详细介绍std::enable_shared_from_this的使用方法、原理和注意事项。

1. 什么是 std::enable_shared_from_this

在现代 C++ 编程中,特别是在多线程环境下,std::enable_shared_from_this 是一个重要的工具。它是一个模板类,定义在 <memory> 头文件中,能够让一个对象在已经有一个 std::shared_ptr 指向它的情况下,通过 shared_from_this() 方法安全地获取额外的 std::shared_ptr。这种方法有效地避免了因对象提前销毁而导致的悬空 std::shared_ptr 访问问题。

以下是来自cplusplus-enable_shared_from_this的介绍

  • 模板类定义
template <class T> class enable_shared_from_this;
  • 功能
    该类提供的功能允许派生类的对象创建指向自身的 shared_ptr 实例,并与现有的 shared_ptr 对象共享所有权。

​ 请注意,简单地返回 shared_ptr<T>(this) 是有问题的,因为这会创建一个不同的所有权组。

  • 官方示例
#include <iostream>
#include <memory>

struct C : std::enable_shared_from_this<C> { };
int main () {
  std::shared_ptr<C> foo, bar;
  foo = std::make_shared<C>();
  bar = foo->shared_from_this();
  if (!foo.owner_before(bar) && !bar.owner_before(foo))
    std::cout << "foo and bar share ownership";
  return 0;
}
  • 输出
foo and bar share ownership

2. std::enable_shared_from_this 的作用

std::enable_shared_from_this 的核心作用在于允许继承了它的类的对象,在已有 std::shared_ptr 管理时,安全地返回一个额外的 std::shared_ptr。这种机制通常在生产者-消费者模型中非常有用,其中生产者创建对象并使用 std::shared_ptr 管理,消费者通过 std::weak_ptr 安全地访问这些对象。

来看下面的一段小程序:

#include <iostream>
#include <memory>

class Test {
public:
    ~Test() { std::cout << "Test Destructor." << std::endl; }

    std::shared_ptr<Test> GetObject() {
        std::shared_ptr<Test> pTest(this);
        return pTest;
    }
};

int main() {
    {
        std::shared_ptr<Test> p(new Test());
        std::shared_ptr<Test> q = p->GetObject();
    }
    return 0;
}

程序输出:

Test Destructor.
Test Destructor.

从上面的输出可以发现,我们只创建了一个 Test 对象,但却调用了两次析构函数。这对程序来说是灾难性的。为什么会出现这种情况呢?在 main 函数中,std::shared_ptr<Test> p(new Test());shared_ptr 中引用计数器的值设置为 1,而在 GetObject 函数中,通过 std::shared_ptr<Test> pTest(this); 又将 shared_ptr 中引用计数器的值增加了 1。因此,在析构时,一个 Test 对象被析构了两次。即产生这个错误的原因是通过同一个 Test 指针对象创建了多个 shared_ptr,这是绝对禁止的。

这也提醒我们在使用 shared_ptr 时,绝对不能通过同一个指针对象创建多个 shared_ptr 对象。那么,有什么方法从一个类的成员函数中获取当前对象的 shared_ptr 呢?其实方法很简单:只需要让该类继承自 enable_shared_from_this 模板类,然后在需要 shared_ptr 的地方调用 enable_shared_from_this 模板类的成员函数 shared_from_this() 即可。下面是改进后的代码:

#include <iostream>
#include <memory>

class Test : public std::enable_shared_from_this<Test> {
public:
    ~Test() { std::cout << "Test Destructor." << std::endl; }

    std::shared_ptr<Test> GetObject() {
        return shared_from_this();
    }
};

int main() {
    {
        std::shared_ptr<Test> p(new Test());
        std::shared_ptr<Test> q = p->GetObject();
    }
    return 0;
}

程序输出:

Test Destructor.

从输出结果可以看出,对象只被析构了一次,这是我们想要的结果。因此,enable_shared_from_this 模板类的作用是:作为一个基类,它允许从一个成员函数中获得当前对象的 shared_ptr

3. 解决悬空指针访问的问题

C++编程:使用std::weak_ptr监控std::shared_ptr解决多线程竞态实例(智能指针失效)中我们提到了悬空指针的问题。

在生产者-消费者模型中的应用示例,假设 Event 结构体继承自 std::enable_shared_from_this<Event>。在生产者线程中,可以这样创建和管理事件对象:

struct Event : std::enable_shared_from_this<Event> {
    int id;
};

std::shared_ptr<Event> event = std::make_shared<Event>();

接着,生产者将事件对象添加到事件队列时,可以使用 shared_from_this() 返回的 std::shared_ptr 作为 std::weak_ptr 存入队列:

std::weak_ptr<Event> weak_event = event->shared_from_this();
event_queue.push_event(event, weak_event);

在消费者线程中,可以安全地使用 std::weak_ptr,并通过 lock() 方法获取 std::shared_ptr

if (auto shared_ptr = weak_event.lock()) {
    // 使用 shared_ptr 安全地访问事件对象
}

这种方式确保了消费者线程在使用事件对象时,不会因为对象提前被销毁而导致悬空指针访问的问题。

4. 无法解决的竞态条件

尽管 std::enable_shared_from_this 能够帮助在多线程环境中安全地获取 std::shared_ptr,但它并不能解决所有的竞态条件问题。在生产者-消费者模型中,仍然需要使用适当的同步机制(如互斥量)来保护共享数据结构,以防止多个线程同时访问和修改数据而引发的竞态条件。

例如,考虑一个简单的生产者-消费者模式,其中多个生产者向队列中添加元素,而多个消费者从队列中取出元素。

假设有一个线程安全的队列类ThreadSafeQueue,它包含一个std::deque作为底层容器,并且生产者和消费者线程都使用std::shared_ptr来管理ThreadSafeQueue的生命周期。即使生产者和消费者使用shared_from_this()来获取std::shared_ptr,他们仍然需要互斥量(mutex)或其他同步原语来保证在任何时候只有一个线程能够修改队列的状态。

#include <deque>
#include <mutex>
#include <condition_variable>
#include <memory>

class ThreadSafeQueue : public std::enable_shared_from_this<ThreadSafeQueue> {
    std::deque<int> data_queue;
    std::mutex mutex;
    std::condition_variable cond;

    void push(int value) {
        std::lock_guard<std::mutex> lock(mutex);
        data_queue.push_back(value);
        cond.notify_one();
    }

    int pop() {
        std::unique_lock<std::mutex> lock(mutex);
        cond.wait(lock, [this] { return !data_queue.empty(); });
        int result = data_queue.front();
        data_queue.pop_front();
        return result;
    }
};

5. enable_shared_from_this的实现原理

5.1 原理阐述

enable_shared_from_this 是一个模板类,允许对象通过 shared_from_this() 成员函数获得指向自身的 shared_ptr。其实现原理与 weak_ptr 密切相关。

当一个对象继承了 enable_shared_from_this,并被一个 shared_ptr 管理时,shared_ptr 会在内部维护一个 weak_ptr 指向该对象。这个 weak_ptrenable_shared_from_this 的成员变量 weak_this_。当调用 shared_from_this() 时,实际上是通过 weak_this_ 创建一个新的 shared_ptr,这样确保了 shared_ptr 的引用计数是正确的。

具体来说,当 shared_ptr 被构造时,它会检测对象是否继承自 enable_shared_from_this,如果是,则初始化 weak_this_ 指向该对象。这样,当 shared_from_this() 被调用时,weak_this_ 就能生成一个新的 shared_ptr,确保引用计数的正确性,避免重复删除对象。

5.2 原理伪代码示例

以下是 enable_shared_from_this 的实现原理的简单伪代码示例:

// 定义 enable_shared_from_this 模板类
template <typename T>
class enable_shared_from_this {
public:
    std::shared_ptr<T> shared_from_this() {
        return weak_this_.lock();  // 使用 weak_ptr 创建 shared_ptr
    }

protected:
    // 构造函数,默认初始化 weak_this_
    enable_shared_from_this() {}

private:
    std::weak_ptr<T> weak_this_;  // 用于存储对象的 weak_ptr

    // 允许 shared_ptr 访问私有成员
    template <typename U>
    friend class std::shared_ptr;
};

// 自定义 shared_ptr 的构造函数
template <typename T>
class shared_ptr {
public:
    shared_ptr(T* ptr) : ptr_(ptr) {
        // 检查对象是否继承自 enable_shared_from_this
        if (auto enable_shared = dynamic_cast<enable_shared_from_this<T>*>(ptr)) {
            enable_shared->weak_this_ = *this;  // 初始化 weak_this_
        }
    }

    // 其他 shared_ptr 成员函数...

private:
    T* ptr_;  // 实际管理的对象指针
};

// 示例类继承 enable_shared_from_this
class MyClass : public enable_shared_from_this<MyClass> {
public:
    void func() {
        std::shared_ptr<MyClass> self = shared_from_this();  // 获取指向自身的 shared_ptr
        // 使用 self 进行操作...
    }
};

// 使用示例
int main() {
    std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
    obj->func();  // 正确使用 shared_from_this()
    return 0;
}

这个伪代码展示了 enable_shared_from_this 的基本工作原理:

  1. enable_shared_from_this 内部维护一个 weak_ptr 成员 weak_this_
  2. 当对象被 shared_ptr 管理时,shared_ptr 会初始化 weak_this_
  3. 调用 shared_from_this() 时,通过 weak_this_ 创建一个新的 shared_ptr,确保引用计数正确。

5.3 原理示意图

以下是一个更直观的图来说明 enable_shared_from_this 的工作原理:

  • 创建 shared_ptr 管理的对象
1. 创建 shared_ptr 管理对象
+--------------------+             +-------------------------+
| shared_ptr<MyClass>|             | MyClass : enable_...    |
|--------------------|             |-------------------------|
| + ref_count = 1    |             | + weak_this_ (未初始化) |
| + ptr ------------>|-----------> | + 其他成员               |
+--------------------+             +-------------------------+

shared_ptr<MyClass> obj = std::make_shared<MyClass>();
  • 初始化 weak_this_
2. shared_ptr 构造函数初始化 weak_this_
+--------------------+             +-------------------------+
| shared_ptr<MyClass>|             | MyClass : enable_...    |
|--------------------|             |-------------------------|
| + ref_count = 1    |             | + weak_this_ ---------->|
| + ptr ------------>|-----------> | + 其他成员               |
|--------------------|             +-------------------------+
  • 调用 shared_from_this
3. 调用 shared_from_this() 获取 shared_ptr
+--------------------+             +-------------------------+
| shared_ptr<MyClass>|             | MyClass : enable_...    |
|--------------------|             |-------------------------|
| + ref_count = 2    |<----------- | + weak_this_ (指向对象)  |
| + ptr ------------>|             | + 其他成员               |
+--------------------+             +-------------------------+

std::shared_ptr<MyClass> self = obj->shared_from_this();
  • 图示说明
  1. 创建 shared_ptr 管理对象

    • 当我们创建一个 shared_ptr 来管理一个继承自 enable_shared_from_this 的对象时,shared_ptr 的构造函数会将对象指针传递给 shared_ptr,并初始化引用计数。
  2. 初始化 weak_this_

    • 如果对象继承自 enable_shared_from_thisshared_ptr 的构造函数会将 enable_shared_from_this 内部的 weak_this_ 初始化为指向该对象的 weak_ptr
  3. 调用 shared_from_this

    • 当我们调用 shared_from_this 时,weak_this_ 会返回一个新的 shared_ptr,这样保证了引用计数的正确管理,避免对象被重复删除。

6. 使用 enable_shared_from_this 常见错误

情形1:

class Test : public std::enable_shared_from_this<Test> {
 public:
  Test() { 
    std::shared_ptr<Test> pTest = shared_from_this(); 
  }
};

这种用法是错误的。虽然对象的基类 enable_shared_from_this 的构造函数已经被调用,但 shared_ptr 的构造函数并没有被调用,因此 weak_ptr weak_this_ 没有被初始化,所以调用 shared_from_this() 是错误的。

情形2:

class Test : public std::enable_shared_from_this<Test> {
 public:
  void func() { 
    std::shared_ptr<Test> pTest = shared_from_this(); 
  }
};

int main() {
  Test test;
  test.func();  // 错误

  std::shared_ptr<Test> pTest = std::make_shared<Test>();
  pTest->func(); // 正确

  return 0;
}

这种做法同样是错误的,原因与情形1相同。shared_ptr 的构造函数并没有被调用,因此 weak_ptr weak_this_ 没有被初始化。

正确做法:

class Test : public std::enable_shared_from_this<Test> {
 public:
  void func() { 
    std::shared_ptr<Test> pTest = shared_from_this(); 
  }
};

int main() {
  std::shared_ptr<Test> pTest = std::make_shared<Test>();
  pTest->func();

  return 0;
}

std::shared_ptr<Test> pTest = std::make_shared<Test>(); 这句话依次执行的顺序是:

  1. 调用 enable_shared_from_this 的构造函数。
  2. 调用 Test 的构造函数。
  3. 调用 shared_ptr 的构造函数初始化 weak_ptr weak_this_

最后才能通过 func() 函数使用 shared_from_this()

其他注意事项:

  1. 不能在对象的构造函数中使用 shared_from_this()
  2. 需要依次调用 enable_shared_from_this 的构造函数、对象的构造函数,最后调用 shared_ptr 的构造函数初始化 enable_shared_from_this 的成员变量 weak_this_,然后才能使用 shared_from_this()
  3. 如果程序中使用了智能指针 shared_ptr,则整个程序应统一使用智能指针,不能使用原始指针,以免出现错误。

7. 总结

使用 std::enable_shared_from_this 是在多线程环境中安全使用 std::shared_ptrstd::weak_ptr 的一种辅助方法。它有效地避免了因对象提前销毁而导致的悬空指针访问问题,提升了程序的稳定性和可靠性。然而,在设计和实现多线程应用时,仍需综合考虑并正确使用同步机制,才能彻底避免竞态条件和其他多线程安全性问题的发生。

8. 参考文章

enable_shared_from_this模板类使用完全解析
C++ enable_shared_from_this解析

相关推荐

  1. C++面试之线池、智能指针、设计模式

    2024-07-20 13:14:05       47 阅读
  2. C++11 在 Windows 环境下的线编程指南

    2024-07-20 13:14:05       27 阅读
  3. c++ 智能指针使用注意事项及解决方案

    2024-07-20 13:14:05       18 阅读
  4. C++ 并发编程指南(8)线间通信

    2024-07-20 13:14:05       37 阅读
  5. c++/c中野指针悬空指针的示例

    2024-07-20 13:14:05       23 阅读
  6. C语言什么是悬空指针

    2024-07-20 13:14:05       53 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 13:14:05       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 13:14:05       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 13:14:05       45 阅读
  4. Python语言-面向对象

    2024-07-20 13:14:05       55 阅读

热门阅读

  1. 定个小目标之刷LeetCode热题(45)

    2024-07-20 13:14:05       20 阅读
  2. 人工势场法路径规划算法

    2024-07-20 13:14:05       13 阅读
  3. Android笔试面试题AI答之Activity(2)

    2024-07-20 13:14:05       17 阅读
  4. HIVE:使用get_json_object解析json对象

    2024-07-20 13:14:05       18 阅读
  5. Elasticsearch索引管理和生命周期管理

    2024-07-20 13:14:05       18 阅读
  6. 现代生活背景下陶瓷艺术设计的延伸与发展

    2024-07-20 13:14:05       19 阅读
  7. LeetCode 2956.找到两个数组中的公共元素:哈希表

    2024-07-20 13:14:05       18 阅读
  8. 麦芒30全新绽放,中国电信勾勒出AI手机的新方向

    2024-07-20 13:14:05       20 阅读
  9. Prometheus 运维中实际的故障案例以及解决办法

    2024-07-20 13:14:05       15 阅读
  10. Gmsh应用程序编程接口

    2024-07-20 13:14:05       12 阅读
  11. 【Go系列】RPC和grpc

    2024-07-20 13:14:05       16 阅读