Boost async_ * функции и shared_ptr's - программирование

Boost async_ * функции и shared_ptr's

Я часто вижу этот шаблон в коде, привязывая 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 имеет тип Connection*, который может принимать все handle_Receive, а возвращаемый умный указатель должен быть немедленно преобразован в обычный указатель. Если это произойдет, ничего не останется в живых. И, конечно, нет указателя на вызов shared_from_this.

Однако, я часто видел эту схему, я не могу поверить, что она полностью сломана, как мне кажется. Есть ли какая-то магия Boost, которая приводит к тому, что shared_ptr преобразуется в обычный указатель позже, когда операция завершается? Если да, то это где-то документировано?

В частности, документировано ли где-то, что общий указатель будет существовать до завершения операции? Вызов get_pointer на сильном указателе, а затем вызов функции-члена по возвращенному указателю недостаточен, если сильный указатель не будет уничтожен до тех пор, пока функция-член не вернется.

4b9b3361

Ответ 1

Короче говоря, boost::bind создает копию boost::shared_ptr<Connection>, которая возвращается из shared_from_this(), а boost::asio может создать копию обработчика. Копия обработчика останется в живых до тех пор, пока не произойдет одно из следующих действий:

  • Обработчик был вызван потоком, из которого были вызваны функции run(), run_one(), poll() или poll_one() службы poll_one().
  • io_service уничтожен.
  • io_service::service, которому принадлежит обработчик, отключается через shutdown_service().

Вот соответствующие выдержки из документации:

  • boost:: bind документация:

    Аргументы, которые принимает bind, копируются и удерживаются внутри объекта возвращаемой функции.

  • boost:: asio io_service::post:

    io_service гарантирует, что обработчик будет вызван только в потоке, в котором в настоящее время вызывается функции-члены run(), run_one(), poll() или poll_one(). [...] io_service сделает копию объекта обработчика по мере необходимости.

  • boost:: asio io_service::~io_service:

    Неинвинированные объекты обработчика, которые были запланированы для отложенного вызова на io_service или любой связанной нити, будут уничтожены.

    Если время жизни объекта привязано к времени жизни соединения (или некоторой другой последовательности асинхронных операций), объект shared_ptr для объекта будет привязан к обработчикам для всех связанных с ним асинхронных операций. [...] Когда одно соединение завершается, все связанные асинхронные операции завершаются. Соответствующие объекты обработчика уничтожаются, и все ссылки shared_ptr на объекты уничтожаются.


Пока датированный (2007), предложение Сетевой библиотеки для TR2 (Редакция 1) было получено из Boost.Asio. Раздел 5.3.2.7. Requirements on asynchronous operations содержит некоторые детали для аргументов функций async_:

В этом разделе асинхронная операция инициируется функцией, которая называется с префиксом async_. Эти функции должны быть известны как инициирующие функции. [...] Реализация библиотеки может делать копии аргумента обработчика и исходный аргумент обработчика и все копии являются взаимозаменяемыми.

Время жизни аргументов для инициирующих функций должно рассматриваться следующим образом:

  • Если параметр объявлен как ссылка на константу или по значению [...], реализация может сделать копии аргумента, и все копии будут уничтожены не позднее, чем сразу после вызова обработчика.

[...] Любые вызовы, выполняемые реализацией библиотеки, в функции, связанные с аргументами инициализирующей функции, будут выполняться таким образом, чтобы вызовы возникали в вызове последовательности 1 для вызова n, где для всех i, 1 ≤ я < n, вызов i предшествует вызову я + 1.

Таким образом:

  • Реализация может создать копию обработчика. В этом примере скопированный обработчик создаст копию shared_ptr<Connection>, увеличив счетчик ссылок экземпляра Connection, пока копии обработчика останутся в живых.
  • Реализация может уничтожить обработчик до вызова обработчика. Это происходит, если операция async выдающаяся, когда io_serive::service выключается или io_service уничтожается. В этом примере копии обработчика будут уничтожены, уменьшится количество ссылок Connection и потенциально приведет к уничтожению экземпляра Connection.
  • Если обработчик вызывается, то все копии обработчика будут немедленно уничтожены после выполнения возврата из обработчика. Опять же, копии обработчика будут уничтожены, уменьшится количество ссылок Connection и потенциально может быть уничтожено.
  • Функции, связанные с аргументами asnyc_, будут выполняться последовательно, а не одновременно. Это включает io_handler_deallocate и io_handler_invoke. Это гарантирует, что обработчик не будет освобожден при вызове обработчика. В большинстве областей реализации boost::asio обработчик копируется или перемещается в стек переменных, позволяя разрушение возникать после того, как выполнение завершает выполнение блока, в котором он был объявлен. В этом примере это гарантирует, что счетчик ссылок для Connection будет по крайней мере один во время вызова обработчика.

Ответ 2

Это происходит следующим образом:

1) Документация Boost.Bind состояния:

"[Примечание: mem_fn создает объекты функций, которые могут принимать указатель, ссылку или умный указатель на объект в качестве первого аргумент; для получения дополнительной информации см. документацию mem_fn.]"

2) документация mem_fn говорит:

Когда объект функции вызывается с первым аргументом x, который является ни указатель, ни ссылка на соответствующий класс (X в пример выше), он использует get_pointer (x) для получения указателя от x. Авторы библиотек могут "регистрировать" свои классы интеллектуальных указателей обеспечивая соответствующую перегрузку get_pointer, позволяя mem_fn узнавать и поддерживать их.

Таким образом, указатель или смарт-указатель хранятся в связующем как есть, до его вызова.

Ответ 3

Я также вижу, что этот шаблон используется много и (благодаря @Tanner). Я вижу, почему он использовался, когда io_service выполняется в нескольких потоках. Тем не менее, я думаю, что с ним все еще возникают проблемы, поскольку он заменяет потенциальный сбой потенциальной утечкой памяти/ресурса...

Благодаря boost:: bind, любые обратные вызовы, привязанные к shared_ptrs, становятся "пользователями" объекта (увеличивая количество объектов use_count), поэтому объект не будет удален до тех пор, пока не будут вызваны все выдающиеся обратные вызовы.

Обратные вызовы функций boost:: asio:: async * вызывается всякий раз, когда отменяется или закрывается вызывается на соответствующий таймер или сокет. Обычно вы просто делаете соответствующие отмены/закрытия вызовов в деструкторе, используя шаблон Straustrup, любимый 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_ptr в классе Connection и передается функции read_callback как shared_ptr.

Это означает, что при выполнении нескольких io_services в отдельных задачах read_buffer_ не удаляется до завершения других задач, то есть когда функция read_callback была вызвана.

Ответ 4

Нет преобразования из boost::shared_ptr<Connection> (возвращаемого типа shared_from_this) в Connection* (тип this), так как это было бы небезопасно, как вы по праву указали.

Магия в Boost.Bind. Проще говоря, при вызове формы bind(f, a, b, c) (в этом примере не задействовано замещающее или вложенное выражение привязки), где f является указателем на член, тогда вызов результата вызова приведет к вызову form (a.*f)(b, c), если a имеет тип, полученный из типа класса указателя на член (или тип boost::reference_wrapper<U>), или же его формы ((*a).*f)(b, c). Это работает с указателями и умными указателями. (Я на самом деле работаю из памяти правил для std::bind, Boost.Bind не совсем идентичен, но оба находятся в одном и том же духе.)

Кроме того, результат shared_from_this() сохраняется в результате вызова bind, гарантируя отсутствие проблем с продолжительностью жизни.

Ответ 5

Возможно, мне не хватает чего-то очевидного здесь, но shared_ptr, возвращаемый shared_from_this(), хранится в объекте функции, возвращаемом boost::bind, который сохраняет его в памяти. Он только неявно преобразуется в Connection* в момент, когда обратный вызов запускается, когда чтение async завершается, и объект сохраняется в живых, по крайней мере, в течение продолжительности вызова. Если handle_Receive не создает другого shared_ptr из этого, а shared_ptr, который был сохранен в функторе связывания, является последним shared_ptr вживую, объект будет уничтожен после возврата обратного вызова.