Увеличьте функции async_ * и shared_ptr

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


person David Schwartz    schedule 06.07.2012    source источник
comment
Когда мы писали сетевой уровень для нашего проекта, мы добавили внешний материал, чтобы гарантировать, что обработчик не разрушился при установлении соединения.   -  person PSyton    schedule 06.07.2012
comment
@PSyton: Я сделал то же самое, написав static вспомогательные функции, которые явно принимают shared_ptr и вызывают функцию-член. Но я вижу так много кода, который делает это таким образом, я не могу поверить, что все это сломано. Тем не менее, я тоже не вижу, где это задокументировано.   -  person David Schwartz    schedule 06.07.2012


Ответы (5)


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

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

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

  • # P3 #
    # P4 #
  • # P5 #
    # P6 #
  • # P7 #
    # P8 #

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

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

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

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

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

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

  • Реализация может создать копию обработчика. В этом примере скопированный обработчик создаст копию shared_ptr<Connection>, увеличивая счетчик ссылок экземпляра Connection, в то время как копии обработчика остаются активными.
  • Реализация может уничтожить обработчик до вызова обработчика. Это происходит, если асинхронная операция не завершена, когда io_serive::service выключен или io_service уничтожен. В этом примере копии обработчика будут уничтожены, уменьшив счетчик ссылок Connection и потенциально вызывая уничтожение экземпляра Connection.
  • Если вызывается обработчик, все копии обработчика будут немедленно уничтожены, как только выполнение возвращается из обработчика. Опять же, копии обработчика будут уничтожены, уменьшив счетчик ссылок Connection и потенциально вызывая его уничтожение.
  • Функции, связанные с аргументами asnyc_, будут выполняться последовательно, а не одновременно. Сюда входят io_handler_deallocate и io_handler_invoke. Это гарантирует, что обработчик не будет освобожден во время вызова обработчика. В большинстве областей реализации boost::asio обработчик копируется или перемещается в переменные стека, что позволяет уничтожению происходить после выхода выполнения из блока, в котором он был объявлен. В примере это гарантирует, что счетчик ссылок для Connection будет по крайней мере одним во время вызова обработчика.
person Tanner Sansbury    schedule 07.07.2012
comment
У кого-нибудь есть мысли по поводу этого паттерна w.r.t. представление? При каждой записи или чтении необходимо будет создавать новый shared_ptr, который имеет дело с атомами / мьютексами и может значительно замедлить работу. У кого-нибудь есть мысли о том, как продолжать передавать один и тот же shared_ptr вместо того, чтобы каждый раз создавать новый? - person schuess; 12.06.2017
comment
@schuess Вы хотите создавать новый shared_ptr каждый раз, потому что вы хотите, чтобы объект оставался живым, и вы хотите удалить последний объект, чтобы удалить объект. На современных ЦП стоимость пренебрежимо мала, потому что почти не будет конкуренции, потому что операции по своей сути быстрые и обычно выполняются в виртуальной цепочке. Такая оптимизация почти всегда будет ошибочной. - person David Schwartz; 27.07.2017
comment
@DavidSchwartz, можно ли объявить этот общий объект как член класса подключения, чтобы избежать использования shared_ptr? - person SAMPro; 06.04.2020
comment
@SAMPro. Это затруднило бы обеспечение достаточно длительного времени жизни разделяемого объекта. Но так можно сделать. - person David Schwartz; 06.04.2020
comment
@DavidSchwartz, у меня есть вопрос по этому поводу, не могли бы вы взглянуть на него? - person SAMPro; 08.04.2020

Это выглядит так:

1) В документации Boost.Bind говорится:

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

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

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

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

person Igor R.    schedule 06.07.2012

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

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

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

person kenba    schedule 27.10.2013
comment
Когда несколько потоков запускают io_service, может возникнуть неопределенное поведение, поскольку время жизни базовой буферной памяти и сокета больше не гарантируется, по крайней мере, столько же, сколько и обработчик. Возможно, стоит подумать об использовании непрозрачного указателя, чтобы отделить желаемую семантику RAII с основным сроком службы Ресурсы. Это позволит закрыть сокет, когда пользовательский код больше не имеет дескриптора для Connection, но все же позволит необходимым ресурсам оставаться действительными на время асинхронных операций. - person Tanner Sansbury; 29.10.2013
comment
Спасибо @Tanner, я не учел время жизни буфера в многопоточном случае. Я изменил свой ответ, чтобы прояснить, что шаблон weak_ptr следует использовать только в однопоточной среде. Меня интересует ваше opaque pointer решение, у вас есть пример? - person kenba; 30.10.2013
comment
Хотя это и не совсем непрозрачный указатель, здесь является быстрым примером, который предоставляет семантику RAII для user, отделив его от времени жизни объекта. - person Tanner Sansbury; 31.10.2013
comment
Спасибо @Tanner, пример был интересным. Однако я чувствовал, что самый простой способ обеспечить поддержку многопоточности в моем ответе - просто поместить буфер в shared_ptr и передать его в bind обратному вызову, чтобы он мог пережить класс Connection. - person kenba; 05.11.2013

Преобразования из 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, что гарантирует отсутствие проблем со временем жизни.

person Luc Danton    schedule 06.07.2012
comment
Проблема в том, когда оценивается *f и каков срок жизни *f. Если это гарантированно именно то, что необходимо для этого использования, где это задокументировано? - person David Schwartz; 06.07.2012
comment
@DavidSchwartz Помните, что существует оболочка вызова (назовем ее w), то есть результат вызова bind, который содержит копию интеллектуального указателя. При выполнении эквивалента w(x, y, z) эта копия не исчезает - w останется в живых, по крайней мере, до конца вызова. - person Luc Danton; 06.07.2012

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

person reko_t    schedule 06.07.2012
comment
Что гарантирует, что объект будет оставаться в живых, по крайней мере, на время вызова? Где это задокументировано? - person David Schwartz; 06.07.2012
comment
Потому что вы не можете вызвать operator() функтора связывания, если объект функции больше не существует. : P Только после возврата operator() из функтора связывания функтор может быть уничтожен логически. - person reko_t; 06.07.2012
comment
Мне неудобно полагаться на рассуждения о том, как функция должна быть реализована на том основании, что мы не можем придумать другого способа сделать это. Мне действительно комфортно полагаться только на гарантированную семантику операции. - person David Schwartz; 11.07.2012