【问题标题】:How to safely use callbacks when the bound function could be deleted可以删除绑定函数时如何安全使用回调
【发布时间】:2020-04-22 08:15:13
【问题描述】:

在下面的代码中,我们正在创建一个对象,绑定一个函数并在删除对象之后调用它。

这显然会导致分段错误,因为删除后使用了底层对象。

在为异步数据提供回调的库的上下文中,我们应该如何防止回调函数指向nullptr

您可以在 cpp.sh/5ubbg 进行测试

#include <memory>
#include <functional>
#include <iostream>

class CallbackContainer {
 public:
  std::string data_;
  CallbackContainer(std::string data): data_(data) {}
  ~CallbackContainer() {}
  void rawTest(const std::string& some_data);
};

void CallbackContainer::rawTest(const std::string& some_data) {
  std::cout << data_ << " " << some_data << std::endl;
}

int main(int /* argc */, char const** /* argv */) {
  std::unique_ptr<CallbackContainer> container;
  container.reset(new CallbackContainer("Internal data"));

  auto callback = std::bind(&CallbackContainer::rawTest, container.get(), std::placeholders::_1);
  callback("Before");
  std::cout << &callback << std::endl;
  container.reset();
  std::cout << &callback << std::endl;
  callback("After");
  return 0;
}

返回:

> Internal data Before 
> 0x7178a3bf6570 
> 0x7178a3bf6570 
> Error launching program (Segmentation fault)

【问题讨论】:

    标签: c++ c++11 callback


    【解决方案1】:

    如果您可以共享所有权,请执行以下操作:

    int main(int /* argc */, char const** /* argv */) {
      std::shared_ptr<CallbackContainer> container; // shared pointer
      container.reset(new CallbackContainer("Internal data"));
      // shared with functor
      auto callback = std::bind(&CallbackContainer::rawTest, container, std::placeholders::_1); 
      callback("Before");
      std::cout << &callback << std::endl;
      container.reset();
      std::cout << &callback << std::endl;
      callback("After");
      return 0;
    }
    

    如果不是,您应该以某种方式将无效性显式传递给函数对象。 这假设您知道容器何时被删除,并在之前手动显式使以下内容无效:

    int main(int /* argc */, char const** /* argv */) {
      std::unique_ptr<CallbackContainer> container;
      container.reset(new CallbackContainer("Internal data"));
      std::atomic<CallbackContainer*> container_raw(container.get());
      auto callback = [&container_raw] (std::string data)
      {
        if (auto c = container_raw.load())
          c->rawTest(data);
      };
      callback("Before");
      std::cout << &callback << std::endl;
      container_raw.store(nullptr);
      container.reset();
      std::cout << &callback << std::endl;
      callback("After");
      return 0;
    }
    

    对于asio情况,通常使用shared_from_this(),如std::bind(&amp;MyClass::MyMemFunc, shared_from_this(), ptr);

    【讨论】:

    • 有一个问题。如果container_raw 也在另一个线程中被访问,因此您正在另一个线程中读取它,那么可以创建race conditions
    • 您需要使用mutex 来避免这种情况。我仍然认为使用shared_ptr 会更好。
    • 我现在同意shared_ptr 对您的情况来说是个好主意。但是在某些情况下您不拥有容器,在这种情况下shared_ptr 可能不方便。我编辑了我的代码以防止出现竞争条件。
    • 不!即使您将其存储在atomic 变量中,有人可能会删除该指针,而您将持有悬空指针!
    • 当然,你要知道其他指针何时被删除,并提前清除atomic。如果您不这样做,即使 mutex 也无济于事,只有 shared_ptr 会起作用。
    【解决方案2】:

    我在使用 boost asio 时喜欢的方式:

    我在使用 boost asio 时遇到了同样的问题。我们需要向io_service 注册回调,并且很难实现某种Manager 类来管理我们可能创建的对象的生命周期。

    所以,我在 cppcon2016 中实现了 Michael Caisse 建议的一些东西。我开始将shared_ptr 传递给std::bind 的对象。

    我曾经延长对象的生命周期,在回调中,您可以决定是再次延长对象的生命周期(通过再次注册回调)还是让它自动死亡。

    std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
    auto func = std::bind(&MyClass::MyMemFunc, this, ptr);
    ptr.reset();
    

    在为异步数据提供回调的库的上下文中,我们应该如何防止回调函数指向 nullptr?

    我不会说这是最好的解决方案,但通过我上面的方法,您可以检测是否需要在回调中继续进行。

    这可能不是有效的方法,但不会导致任何未定义的行为。

    void CallbackContainer::rawTest(const std::string& some_data, std::shared<CallbackContainer> ptr) 
    {
        if (ptr.use_count() == 1) {
            // We are the only owner of the object.
            return; // and the object dies after this
        }
        std::cout << data_ << " " << some_data << std::endl;
    }
    

    编辑:

    一个示例代码展示了如何使用std::enable_shared_from_this

    #include <iostream>
    #include <memory>
    #include <functional>
    
    
    class ABCD: public std::enable_shared_from_this<ABCD> {
    public:
        void call_me_anytime()
        {
            std::cout << "Thanks for Calling Me" << std::endl;
        }
    
    public:
        ABCD(void)
        {
            std::cout << "CONSTRUCTOR" << std::endl;
        }
    
        ~ABCD(void)
        {
            std::cout << "DESTRUCTOR" << std::endl;
        }
    };
    
    int main(void)
    {
        auto ptr = std::make_shared<ABCD>();
        auto cb = std::bind(&ABCD::call_me_anytime, ptr->shared_from_this());
        ptr.reset();
    
        std::cout << "RESETING SHARED_PTR" << std::endl;
        std::cout << "CALLING CALLBACK" << std::endl;
        cb();
        std::cout << "RETURNING" << std::endl;
        return 0;
    }
    

    输出:

    CONSTRUCTOR
    RESETING SHARED_PTR
    CALLING CALLBACK
    Thanks for Calling Me
    RETURNING
    DESTRUCTOR
    

    【讨论】:

    • 恐怕它没有回答这个问题。您正在延长对象的生命周期,而不会阻止在重置指针时调用该函数。我希望像if (safe to call) callback(); else throw;
    • 在回调中,我们可以决定是否需要继续!检查已通过的shared_ptr的引用计数?
    • 好的。但是如果你有两个指向同一个错误指针的绑定怎么办。比你有两个参考计数。您必须实现一些逻辑来计算绑定与引用计数并决定何时安全。我认为一定有更简单的方法。
    • 我怀疑还有其他一些不错的方法来实现它。我认为需要围绕shared_ptr 做一些事情。
    • 乐于学习。如果有人发布了更好的答案,我会删除我的!
    【解决方案3】:

    作为后续,我们决定使用roscpp method,这与 Alex Guteniev 的提议类似。

    我们没有显式地使用std::bind,而是在内部使用它并将父级保持为std::weak_ptr&lt;const void&gt; 指向std::shared_ptr&lt;P&gt; 的指针(因为它会与unique_ptr 冲突)。

    API 看起来像:

      std::shared_ptr<Container> container;
      queue.subscribe(&Container::callback_method, container);
    

    订阅函数如下,T 是数据的显式类型(Class-wise),而 P 是 Parent 类的隐式类(在本例中为 Container)。

    template <class P>
    std::shared_ptr<ThreadedQueue<T>> subscribe(void (P::*function_pointer)(std::shared_ptr<const T>), std::shared_ptr<P> parent, size_t queue_size = -1) {
      callback_ = std::bind(function_pointer, parent.get(), std::placeholders::_1);
      parent_ = std::weak_ptr<const void>(parent);
    }
    

    在调用回调时,我们做如下检查:

    if(auto lock = parent_.lock()) {
      callback_(data);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-08
      • 2021-12-14
      • 2010-09-26
      • 1970-01-01
      • 1970-01-01
      • 2020-08-13
      相关资源
      最近更新 更多