提升Boost async_*函数和shared_ptr的效率

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 的唯一原因是为了在成员函数调用之前保持对象的生命期。但除非某种boost魔法出现,因为this指针的类型是Connection*,那么所有handle_Receive可以接受的只有这个,所以返回的智能指针应该立即转换为常规指针。如果发生这种情况,就没有任何方法来保持对象的生命。当然,在调用shared_from_this时也没有指针存在。

然而,我已经经常看到这种模式,我不相信它完全如我所见那样有问题。是否有某种Boost魔法会导致在操作完成后将shared_ptr转换为常规指针?如果是这样的话,这个被记录在哪里呢?

特别地,文档是否记录了共享指针将保持存在直到操作完成?仅仅调用强指针上的get_pointer并在返回的指针上调用成员函数不足以保证,除非强指针在成员函数返回前没有被销毁。


当我们为项目编写网络层时,我们添加了外部内容来确保在连接建立时处理程序未被销毁。 - PSyton
@PSyton: 我也是这样做的,编写了带有shared_ptr参数的“静态”辅助函数,并调用成员函数。但我看到有很多代码都是这样做的,我不敢相信它们全部都是有问题的。然而,我也找不到文档说明它是如何工作的。 - David Schwartz
5个回答

21
简而言之,boost::bind 创建了一个从 shared_from_this() 返回的 boost::shared_ptr<Connection> 的副本,而 boost::asio 可能会创建处理程序的副本。处理程序的副本将一直保持活动状态,直到发生以下情况之一:
  • 处理程序已被调用,并且调用的线程是服务的 run()run_one()poll()poll_one() 成员函数之一。
  • io_service 被销毁。
  • 通过 shutdown_service() 关闭拥有处理程序的 io_service::service
以下是文档中的相关摘录:
  • boost::bind 文档 :

    bind 所使用的参数会被复制并由返回的函数对象内部保存。

  • io_service::post :

    io_service 保证该处理程序只会在当前正在调用的 run()run_one()poll()poll_one() 成员函数的线程中被调用。[...] io_service 将根据需要复制处理程序对象。

  • boost::asio io_service::~io_service:

    尚未调用的处理程序对象,它们被预定在io_service或任何关联的strand上推迟执行,并将被销毁。

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


  • 虽然时间较早(2007年),但TR2网络库提案(修订版1)是由Boost.Asio衍生而来。其中第5.3.2.7节详细说明了async_函数的参数:

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

    对于启动函数的参数的生命周期应按如下方式处理:

    • 如果将参数声明为const引用或by-value [... ]实现可以复制参数,并且所有副本必须在处理程序调用后立即销毁。

    [...] 库实现调用与起始函数参数相关的函数时,将按顺序执行这些调用,顺序为从调用1到调用n,其中对于所有的i,1 ≤ i < n,调用i在调用i+1之前。

    因此:

    • 库实现可能会创建handler的副本。在示例中,复制的handler将创建一个指向Connection实例的shared_ptr<Connection>的副本,同时增加引用计数,只要handler的副本保持有效状态。
    • 如果异步操作未完成,当io_service::service关闭或io_service被销毁时,库实现可能会在调用handler之前销毁handler。在示例中,handler的副本将被销毁,减少Connection的引用计数,并可能导致Connection实例被销毁。
    • 如果调用了handler,则返回后立即销毁所有handler的副本。同样,handler的副本将被销毁,减少Connection的引用计数,并可能导致其被销毁。
  • async_的参数关联函数将按顺序执行,而非并发执行。这包括io_handler_deallocateio_handler_invoke。这保证了在调用处理程序时,处理程序不会被释放。在boost::asio实现的大多数区域中,处理程序被复制或移动到堆栈变量中,使得销毁可以在声明它的块退出执行后发生。在本例中,这确保了在调用处理程序期间,Connection的引用计数至少为1。

  • 有没有人对这个模式的性能有什么想法?每次写入或读取都需要创建一个新的shared_ptr,这会涉及到原子操作/互斥锁,可能会严重拖慢速度。有没有人有关于如何保持传递相同shared_ptr的想法,而不是每次都创建一个新的? - schuess
    @schuess 您想每次创建一个新的 shared_ptr,因为您想保持对象的存活,并且您希望删除最后一个对象时将其删除。在现代 CPU 上,成本是可以忽略不计的,因为几乎没有争用,因为操作本质上是快速的,并且通常在虚拟线程上进行。这样的“优化”几乎总是误导人的。 - David Schwartz
    @DavidSchwartz,是否可以将共享对象声明为连接类的成员,以避免使用shared_ptr - SAMPro
    @SAMPro 这会使得确保共享对象具有足够长的生命周期变得更加困难。但是可以用这种方式完成。 - David Schwartz
    @DavidSchwartz,我有一个关于这个问题的问题,您介意看一下吗? - SAMPro

    5

    内容如下:

    1)Boost.Bind文档说明

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

    2)mem_fn文档指出

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

    因此,在调用之前,指针或智能指针按原样存储在绑定器中。


    4
    我也经常看到这种模式的使用,(感谢@Tanner)我可以看出当io_service多个线程中运行时为什么要使用它。然而,我认为仍然存在生命周期问题,因为它将潜在的崩溃替换为潜在的内存/资源泄漏...
    由于boost::bind,绑定到shared_ptrs的任何回调都成为对象的“用户”(增加对象的use_count),因此直到所有未完成的回调被调用后才删除对象。
    boost :: asio :: async *函数的回调在相关计时器或套接字上调用cancel或close时调用。通常,您只需使用Stroustrup所钟爱的RAII模式在析构函数中进行适当的取消/关闭调用即可完成工作。
    然而,当所有者删除对象时,析构函数不会被调用,因为回调仍然持有shared_ptr的副本,因此它们的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_)));
      }
    };
    

    注意:在 Connection 类中,read_buffer_ 存储为 shared_ptr,并作为 shared_ptr 传递给 read_callback 函数。这是为了确保在多个 io_services 在单独的任务中运行时,read_buffer_ 直到其他任务完成后才被删除,即在调用 read_callback 函数时。

    3
    当多个线程运行io_service时,由于底层缓存内存和套接字的生命周期不再保证与处理程序至少同样长,因此可能会出现未定义的行为。值得考虑使用不透明指针来解耦所需的RAII语义和资源的底层寿命。这将允许在用户代码不再拥有Connection句柄时关闭套接字,但仍允许必要的资源在异步操作期间保持有效。 - Tanner Sansbury
    感谢@Tanner的提醒,我没有考虑到多线程情况下的缓存生命周期。我已经修改了我的回答,明确弱引用模式应该只在单线程环境下使用。我对您的“不透明指针”解决方案很感兴趣,您有例子吗? - kenba
    虽然不完全是一个不透明指针,但在这里有一个快速示例,为用户提供RAII语义,同时将其与对象的生命周期分离。 - Tanner Sansbury
    谢谢@Tanner,这个例子很有趣。然而,我觉得在我的答案中提供多线程支持的最简单方法就是将缓冲区放在shared_ptr中,并将其传递到回调函数中的bind中,以便它可以超出Connection类的生命周期。 - kenba

    2

    boost::shared_ptr<Connection>(shared_from_this的返回类型)与Connection*(this的类型)之间不存在转换,正如您所指出的那样,这将是不安全的。

    魔法在于Boost.Bind。简单来说,在调用形式为bind(f, a, b, c)的调用中(此示例未涉及占位符或嵌套绑定表达式),其中f是成员指针,则调用调用结果将导致调用(a.*f)(b, c)的形式,如果a具有从成员指针派生的类类型(或类型boost::reference_wrapper<U>),否则它的形式为((*a).*f)(b, c)。这适用于指针和智能指针。 (我实际上是根据对std::bind规则的记忆工作的,Boost.Bind并不完全相同,但两者在本质上是相同的。)

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


    问题在于何时评估 *f,以及 *f 的生命周期是什么。如果这些保证是本用途所需的,那么在哪里可以找到相关文档? - David Schwartz
    @DavidSchwartz 请记住,有一个调用包装器(我们称之为 w),即对 bind 的调用的结果,它持有智能指针的副本。当执行类似于 w(x, y, z) 的操作时,该副本不会消失 - w 将至少保持到调用结束。 - Luc Danton

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

    什么可以确保对象在调用期间至少保持活动状态?这个文档在哪里记录? - David Schwartz
    4
    如果函数对象不存在了,你就不能调用绑定器的operator()了。只有在绑定器的operator()返回之后,这个函数对象才能被逻辑上销毁。请注意不要改变原文的意思。 - reko_t
    我不太喜欢依赖于对函数实现方式的推理,因为我们想不到其他的方法。我只喜欢依赖于操作的保证语义,这样才感到舒适。 - David Schwartz

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接