Я пытаюсь понять различные сценарии циклических ссылок при использовании ioContext
и shared_ptr
и наткнулся на это.
В приведенном ниже коде класс Test имеет переменную-член shared_ptr<io_context>
. Когда я делаю лямбда-пост внутри функции ioContext
внутри async_wait
, я передаю shared_from_this()
{который является Test} в нем. Представьте, что теперь ioContext
выполняется в другом потоке, поэтому лямбда добавляется во внутреннюю ioContext
очередь. Насколько я понимаю, лямбда попадает во внутреннюю очередь ioContext
, и теперь есть циклическая ссылка Test --> ioContext --> лямбда --> Test.
Правильно ли я понимаю здесь? Есть ли способ избежать этого?
https://coliru.stacked-crooked.com/a/586afa794d15ef8f
#include <boost/asio.hpp>
#include <boost/date_time.hpp>
#include <iostream>
std::string getTime()
{
return std::string("[").append(to_iso_extended_string(boost::posix_time::microsec_clock::universal_time())).append("] ");
}
class Test : public std::enable_shared_from_this<Test> {
public:
Test(const std::shared_ptr<boost::asio::io_context>& ioContext)
: ioContext_(ioContext), timer_(*ioContext) {}
void start() {
std::cout << getTime() << "at start\n";
timer_.expires_after(std::chrono::seconds(3));
timer_.async_wait([this, self=shared_from_this()](const boost::system::error_code& ec)
{
std::cout << getTime() << "Inside wait!\n";
if (!ec)
{
ioContext_->post([this, self=shared_from_this()]()
{
printInPost();
});
}
});
}
void printInPost() {
std::cout << getTime() << "I get printed!\n";
}
private:
std::shared_ptr<boost::asio::io_context> ioContext_;
boost::asio::steady_timer timer_;
};
int main() {
std::shared_ptr<boost::asio::io_context> ioContext = std::make_shared<boost::asio::io_context>();
std::shared_ptr<Test> myTest = std::make_shared<Test>(ioContext);
myTest->start();
ioContext->run();
}
@ÖöTiib Я думал об этом, но единственная проблема, с которой я столкнулся при использовании слабого_ptr, заключается в том, что он делает код (особенно тот, в котором есть вложенные асинхронные операции) трудным для чтения и уродливым. Поэтому мне было интересно, есть ли лучший способ связать Shared_from_this с io_context, не беспокоясь об утечке.
Я не знаю. Это... if (auto self =weakself.lock()) else return; ... очень похоже на Swift... Guard let self else { return } ... и Swift, ИМХО, делает это самым лаконичным образом. Это некрасиво и трудно читать?
Есть только цикл, пока io_context
не завершит эту работу. Как только задача будет завершена, она будет удалена из очереди, а последняя копия лямбды будет уничтожена. Это желательно, так как гарантирует, что экземпляр Test
, над которым вы работаете, останется активным до тех пор, пока выполняется задача.
Н.б. вам не нужно захватывать this
в лямбда-выражениях, self
также является указателем на тот же объект.
timer_.async_wait([self=shared_from_this()](const boost::system::error_code& ec)
{
std::cout << getTime() << "Inside wait!\n";
if (!ec)
{
self->ioContext_->post([self]()
{
self->printInPost();
});
}
});
self
также является указателем на тот же объект и более подробен в использовании.
Проблема, о которой я думал, заключалась в том, что если это произойдет сейчас, когда поток, владеющий Test, будет уничтожен (по какой-то причине) вместе с ioContext до того, как лямбда-выражение будет выполнено, и новый поток с новым Test и ioContext теперь будет динамически создан процессом, выполняющим там потоки. будет утечка памяти, поскольку ни старый ioContext, ни старый Test не будут уничтожены? Разве это не правильно?
@proton Было бы неопределенным поведением разрушать io_context
, пока в нем еще есть std::shared_ptr
, и выполнять задачи. Если вы имели в виду «выпустить shared_ptr
», а не «уничтожить», тогда да, пока была незавершенная работа, у вас была бы утечка. В конце концов он очистится сам. Если вы хотите иметь возможность отменить работу, вам необходимо передать токены отмены
@3CxEZiVlQ да, boost документирует, что задачи выводятся из очереди после завершения.
извини, я имел в виду выпуск. В этой связи, если теперь Shared_ptr для тестирования получит новый назначенный ему Shared_ptr и новый ioContext также будет создан до того, как старая ожидающая работа будет завершена, то утечка будет существовать до тех пор, пока процесс не завершится или не перезапустится. Будет ли это считаться неправильным использованием ioContext или это значит, что Shared_from_this не следует использовать с ioContext?
@proton да, как-то странно выпускать io_context
. Если да, сначала вызовите stop, так как это приведет к остановке работы и освобождению всех ожидающих завершения обработчиков.
@Caleth Когда вы говорите «... освободить все ожидающие завершения обработчики», не могли бы вы уточнить (например, удаляет ли это работу из внутренней очереди?). Насколько я понимаю, io_context::stop() только предотвращает отправку новой работы, поэтому ожидающие обработчики, которые уже были в очереди, все равно останутся в очереди? Я здесь неправильно понимаю?
@proton также выпустит ожидающие вещи, например. если время таймера не истекло, внешняя лямбда будет удалена.
@Caleth, но что, если сработает таймер и будет выполнен почтовый вызов io_context, который добавит лямбду во внутреннюю очередь, а затем произойдет сбой, и он останется в очереди? Тогда лучше ли иметь переменную-член ioContext в классе Test как boost::asio::io_context& вместо Shared_ptr<boost::asio::io_context> ?
Давайте продолжим обсуждение в чате.
Никогда не сохраняйте контекст выполнения. Вместо этого передайте исполнителям. Время жизни контекста не входит в обязанности задач.
@sehe не является переменной-членом ioContext_ в классе Test, которая также выступает в качестве исполнителя, поскольку она предоставляет метод post? Я могу ошибаться, но не могли бы вы уточнить: «Никогда не сохранять контекст выполнения...». Как бы мы написали класс Test, чтобы мы «Передавали исполнителей...» Предлагаете ли вы передать io_context в start() -->async_wait() --->lambda по значению (в аргументе или захвате) вместо сохранения это как переменная-член в тесте?
Исполнители — это небольшие, дешево копируемые объекты, которые могут инкапсулировать больше семантики поверх контекста выполнения, а также отделить контекст выполнения от объектов ввода-вывода. Я посмотрю ваш пример кода
Опубликовал свой ответ ниже
Асинхронные операции не несут ответственности за время существования контекста выполнения. Упростить и исправить можно следующим образом:
#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using namespace std::chrono_literals;
static auto elapsedSeconds() {
static auto start = std::chrono::steady_clock::now();
return (std::chrono::steady_clock::now() - start) / 1.s;
}
static int generateId() {
static std::atomic_int id;
return ++id;
}
struct Test : std::enable_shared_from_this<Test> {
Test(asio::any_io_executor ex) : timer_(ex) {}
void start() {
trace() << "start" << std::endl;
timer_.expires_after(3s);
timer_.async_wait([this, self = shared_from_this()](boost::system::error_code const& ec) {
trace() << "Inside wait! (" << ec.message() << ")" << std::endl;
if (!ec)
post(timer_.get_executor(), [this, self] { printInPost(); });
});
}
private:
int const id_ = generateId();
asio::steady_timer timer_;
void printInPost() const {
trace() << "I get printed!" << std::endl;
}
std::ostream& trace() const {
return std::cout << "id:" << id_ << " " << elapsedSeconds() << " ";
}
};
int main() {
std::cout << std::fixed;
{
asio::io_context ioc;
std::make_shared<Test>(ioc.get_executor())->start();
ioc.run();
}
// look ma, decoupled!
{
asio::thread_pool ioc(4); // use 4 threads
std::make_shared<Test>(ioc.get_executor())->start();
ioc.join();
}
}
Печать ожидаемого:
id:1 0.000002 start
id:1 3.000153 Inside wait! (Success)
id:1 3.000226 I get printed!
id:2 3.000516 start
id:2 6.000720 Inside wait! (Success)
id:2 6.000794 I get printed!
Некоторые подробные сведения, начиная с Асинхронные операции:
Вместо этого асинхронные операции принадлежат контексту. Асинхронные операции инициируются функциями инициализации. Все функции инициации ставят в очередь хотя бы один обработчик.
Обработчик гарантированно будет вызван, если контекст не остановлен.
В нашем коде run()
и join()
возвращаются только тогда, когда контекст заканчивается, то есть после завершения.
Разрушение контекста выполнения безопасно, если ни один поток не запускает планировщик. Деструктор освободит все ожидающие обработчики.
ОБНОВЛЯТЬ
В комментарии @Caleths, чтобы гарантировать, что контекст не будет завершен, владелец исполнителя может сохранить work_guard например. см. make_work_guard . Также посмотрите больше предыстории, чем вам, вероятно, понадобится
На той же странице документации есть подробные сведения об этом. Часто асинхронные операции более отслеживают состояние и могут владеть временными ресурсами. Модель асинхронного завершения требует, чтобы эти ресурсы были освобождены до вызова обработчика завершения:
Если асинхронная операция требует временного ресурса (например, памяти, файлового дескриптора или потока), этот ресурс освобождается перед вызовом обработчика завершения.
[...]
Обеспечивая освобождение ресурсов до запуска обработчика завершения, мы избегаем удвоения пикового использования ресурсов в цепочке операций.
Отсюда следует, что обработчики исключаются из очереди перед вызовом.
Я думаю, вам следует конкретно указать, что все, что обеспечивает исполнителю, является долгоживущим объектом, а не чем-то, что может привести к висячим ссылкам.
@Caleth У вас есть конкретное предложение вместо двух предложений, которые я использовал, чтобы закрепить обе стороны: (практично) «Обработчик гарантированно будет вызван, если контекст не остановлен. В нашем коде run() и join() возвращайтесь только тогда, когда контекст исчерпает работу, то есть после завершения». (против систематического :) «Уничтожение контекста выполнения безопасно, если ни один поток не запускает планировщик. Деструктор освободит все ожидающие обработчики».
«безопасно, когда ни один поток не может выполнять новую работу» или «безопасно, когда ни один поток не запускает планировщик». Можно спрятать где-нибудь исполнителя, и в ответ на какой-то независимый стимул выстроить в очередь дополнительную работу.
Ваша фрагментная цитата посвящена разрушению контекста, но я понимаю вашу точку зрения. @Caleth обновлен некоторыми дополнительными примечаниями
Разработанный способ избежать циклических ссылок на std::shared_ptr — это std::weak_ptr. Я просто использую его всякий раз, когда возникает вопрос о возможности циклической ссылки. Итак,weak_from_this — это более идиоматический захват в моем коде.