20

我经常在代码中看到这种模式,将其作为第一个参数绑定shared_from_this到成员函数并使用async_*函数调度结果。这是另一个问题的示例:

void Connection::Receive()
{
     boost::asio::async_read(socket_,boost::asio::buffer(this->read_buffer_),
        boost::bind(&Connection::handle_Receive, 
           shared_from_this(),
           boost::asio::placeholders::error,
           boost::asio::placeholders::bytes_transferred));
 }

使用shared_from_this()代替的唯一原因this是在成员函数被调用之前保持对象处于活动状态。但是除非某处有某种增强魔法,因为this指针是 type Connection*,这一切都handle_Receive可以接受,并且返回的智能指针应该立即转换为常规指针。如果发生这种情况,则没有任何东西可以使对象保持活力。而且,当然,在调用中没有指针shared_from_this

但是,我经常看到这种模式,我不敢相信它像我认为的那样完全被破坏了。当操作完成时,是否有一些 Boost 魔术导致 shared_ptr 稍后转换为常规指针?如果是这样,这是否记录在某处?

特别是,是否在某处记录了共享指针将保持存在直到操作完成?调用get_pointer强指针然后在返回的指针上调用成员函数是不够的,除非强指针在成员函数返回之前没有被销毁。

4

5 回答 5

21

简而言之,boost::bind创建boost::shared_ptr<Connection>从 返回的 的副本shared_from_this(),并且boost::asio可以创建处理程序的副本。处理程序的副本将保持活动状态,直到发生以下情况之一:

  • 处理程序已被调用服务的run()run_one()或成员函数的线程调用poll()poll_one()
  • io_service摧毁。
  • io_service::service拥有处理程序的 是通过 关闭的shutdown_service()

以下是文档的相关摘录:

  • boost::bind文档

    接受的参数bind由返回的函数对象在内部复制和保存。

  • 升压::asio io_service::post:

    io_service保证仅在当前调用 、 或 成员函数的线程中run()调用处理程序run_one()。[...]将根据需要制作处理程序对象的副本。poll()poll_one()io_service

  • 升压::asio io_service::~io_service:

    io_service计划在或任何关联链 上延迟调用的未调用处理程序对象将被销毁。

    如果对象的生命周期与连接的生命周期(或某些其他异步操作序列)shared_ptr相关联,则对象的 a 将绑定到与之关联的所有异步操作的处理程序中。[...]当单个连接结束时,所有关联的异步操作都会完成。相应的处理程序对象被销毁,并且shared_ptr对对象的所有引用都被销毁。


虽然过时(2007 年),但TR2 的网络库提案(修订版 1)源自 Boost.Asio。部分5.3.2.7. Requirements on asynchronous operations提供了async_函数参数的一些细节:

在此子句中,异步操作由以前缀命名的函数启动async_。这些功能应称为启动功能。[...] 库实现可能会复制处理程序参数,并且原始处理程序参数和所有副本都是可互换的。

初始化函数的参数的生命周期应如下处理:

  • 如果参数被声明为 const 引用或按值 [...],则实现可能会复制参数,并且所有副本应在调用处理程序后立即销毁。

[...] 库实现对与启动函数的参数相关联的函数进行的任何调用都将被执行,以便调用按调用1到调用n的顺序发生,其中对于所有i, 1 ≤ i < n,调用i优先调用i+1

因此:

  • 该实现可能会创建处理程序的副本。在示例中,复制的处理程序将创建 的副本shared_ptr<Connection>,增加实例的引用计数,同时处理程序Connection的副本保持活动状态。
  • 该实现可能会在调用handler之前销毁处理程序。如果异步操作在io_serive::service关闭或被io_service销毁时未完成,则会发生这种情况。在示例中,处理程序的副本将被销毁,从而减少 的引用计数Connection,并可能导致Connection实例被销毁。
  • 如果调用了处理程序,则处理程序的所有副本将在处理程序执行返回后立即销毁。同样,处理程序的副本将被销毁,从而减少 的引用计数Connection,并可能导致它被销毁。
  • 与 的参数关联的函数asnyc_将按顺序执行,而不是并发执行。这包括io_handler_deallocateio_handler_invoke。这保证了在调用处理程序时不会释放处理程序。boost::asio实现的大多数区域中,处理程序被复制或移动到堆栈变量,一旦执行退出声明它的块,就允许销毁发生。在示例中,这确保了在调用handlerConnection期间的引用计数至少为 1 。
于 2012-07-07T03:36:53.427 回答
5

它是这样的:

1) Boost.Bind 文档状态

“[注意:mem_fn 创建的函数对象能够接受指向对象的指针、引用或智能指针作为其第一个参数;有关其他信息,请参阅 mem_fn 文档。]”

2) mem_fn 文档

当使用第一个参数 x调用函数对象时,该参数既不是指针也不是对适当类的引用(上例中的 X),它使用 get_pointer(x) 从 x 获取指针。库作者可以通过提供适当的 get_pointer 重载来“注册”他们的智能指针类,从而允许 mem_fn 识别和支持它们。

因此,指针或智能指针按原样存储在活页夹中,直到被调用。

于 2012-07-06T14:58:04.450 回答
4

我还看到这种模式被大量使用并且(感谢@Tanner)我可以看到为什么在多线程io_service中运行时使用它。但是,我认为它仍然存在生命周期问题,因为它用潜在的内存/资源泄漏代替了潜在的崩溃......

感谢 boost::bind,绑定到 shared_ptrs 的任何回调都成为对象的“用户”(增加对象 use_count),因此在调用所有未完成的回调之前不会删除该对象。

每当在相关计时器或套接字上调用取消或关闭时,都会调用 boost::asio::async* 函数的回调。通常,您只需使用 Stroustrup 钟爱的RAII模式在析构函数中进行适当的取消/关闭调用;任务完成。

但是,当所有者删除对象时不会调用析构函数,因为回调仍然保存 shared_ptrs 的副本,因此它们的 use_count 将大于零,从而导致资源泄漏。通过在删除对象之前进行适当的取消/关闭调用,可以避免泄漏。但这并不像使用 RAII 并在析构函数中进行取消/关闭调用那样万无一失。确保资源始终被释放,即使存在异常。

符合 RAII 的模式是使用静态函数进行回调,并在注册回调函数时将 weak_ptr 传递给 boost::bind,如下例所示:

class Connection : public boost::enable_shared_from_this<Connection>
{
  boost::asio::ip::tcp::socket socket_;
  boost::asio::strand  strand_;
  /// shared pointer to a buffer, so that the buffer may outlive the Connection 
  boost::shared_ptr<std::vector<char> > read_buffer_;

  void read_handler(boost::system::error_code const& error,
                    size_t bytes_transferred)
  {
    // process the read event as usual
  }

  /// Static callback function.
  /// It ensures that the object still exists and the event is valid
  /// before calling the read handler.
  static void read_callback(boost::weak_ptr<Connection> ptr,
                            boost::system::error_code const& error,
                            size_t bytes_transferred,
                            boost::shared_ptr<std::vector<char> > /* read_buffer */)
  {
    boost::shared_ptr<Connection> pointer(ptr.lock());
    if (pointer && (boost::asio::error::operation_aborted != error))
      pointer->read_handler(error, bytes_transferred);
  }

  /// Private constructor to ensure the class is created as a shared_ptr.
  explicit Connection(boost::asio::io_service& io_service) :
    socket_(io_service),
    strand_(io_service),
    read_buffer_(new std::vector<char>())
  {}

public:

  /// Factory method to create an instance of this class.
  static boost::shared_ptr<Connection> create(boost::asio::io_service& io_service)
  { return boost::shared_ptr<Connection>(new Connection(io_service)); }

  /// Destructor, closes the socket to cancel the read callback (by
  /// calling it with error = boost::asio::error::operation_aborted) and
  /// free the weak_ptr held by the call to bind in the Receive function.
  ~Connection()
  { socket_.close(); }

  /// Convert the shared_ptr to a weak_ptr in the call to bind
  void Receive()
  {
    boost::asio::async_read(socket_, boost::asio::buffer(read_buffer_),
          strand_.wrap(boost::bind(&Connection::read_callback,
                       boost::weak_ptr<Connection>(shared_from_this()),
                       boost::asio::placeholders::error,
                       boost::asio::placeholders::bytes_transferred,
                       read_buffer_)));
  }
};

注意:read_buffer_存储为shared_ptrConnection 类中的 a 并作为 a 传递给read_callback函数shared_ptr

这是为了确保在多个io_services单独任务中运行的情况下,直到其他任务完成后,即调用函数时,才会删除 read_buffer_read_callback

于 2013-10-27T18:56:29.203 回答
2

没有从boost::shared_ptr<Connection>(的返回类型shared_from_this)到Connection*(的类型this)的转换,因为正如您正确指出的那样,这将是不安全的。

魔法就在 Boost.Bind 中。简而言之,在表单调用中bind(f, a, b, c)(本示例不涉及占位符或嵌套绑定表达式) where fis a pointer to member,然后调用调用结果将导致调用表单(a.*f)(b, c)ifa具有派生的类型从指向成员(或类型boost::reference_wrapper<U>)的指针的类类型,否则它的形式为((*a).*f)(b, c). 这适用于指针和智能指针。(我实际上是在记忆中的规则std::bind,Boost.Bind 并不完全相同,但两者的精神相同。)

此外, 的结果shared_from_this()存储在对 的调用结果中bind,确保没有生命周期问题。

于 2012-07-06T07:44:14.193 回答
1

也许我在这里遗漏了一些明显的东西,但是由返回的 shared_ptrshared_from_this()存储在由返回的函数对象中boost::bind,这使它保持活动状态。它仅在异步读取完成时启动回调时隐式转换为Connection*,并且对象至少在调用期间保持活动状态。如果handle_Receive没有从中创建另一个 shared_ptr,并且存储在绑定函子中的 shared_ptr 是最后一个活动的 shared_ptr,则该对象将在回调返回后被销毁。

于 2012-07-06T07:41:38.940 回答