Сказ об опасном std::enable_shared_from_this, или антипаттерн «Зомби» — разбор полётов

от автора

В настоящей статье приводится разбор вариантов устранения антипаттерна «Зомби», описанного в первой части: Сказ об опасном std::enable_shared_from_this, или антипаттерн «Зомби».

Введение

Весь код, приведённый в статье, опубликован на гитхабе в ветках. В коде использованы несколько новшеств C++17 — weak_from_this(), if statement with init-statement, может что-то ещё по мелочи.

В примерах из первой части статьи цепочка возникновения проблемного кода выглядит следующим образом:
— задумывается класс асинхронного (неблокирующего) выполнения какого-то длительного процесса;
— для придания процессу асинхронности используется отдельный поток выполнения (std::thread);
— для удобства все нужные для работы процесса данные складываются в поля класса;
— в деструкторе класса предусматривается семафор для информирования потока о необходимости досрочного прекращения процесса в случае уничтожения экземпляра класса;
— поток выполнения нуждается в гарантии валидности экземпляра класса, для чего применяется захват сильной ссылки shared_from_this;
— объект потока выполнения является полем данных класса.
Возникает циклическая ссылка. Уничтожение вышестоящей бизнес-логикой всех сильных ссылок на экземпляр класса не приводит к вызову его деструктора. Неожиданно для разработчика попытка досрочного прекращения процесса в соответствии с RAII не срабатывает. Поток выполнения продолжает работу, что вызывает:
— труднодетектируемую утечку ресурсов (всегда);
— неопределённое поведение (в зависимости от применения);
— сбой высокоуровневой логики (в зависимости от применения).
Вот эту ситуацию и будем развязывать.

Способ очевидный, или weak

Одним из основных элементов антипаттерна «Зомби» является циклическая ссылка. Стандартной техникой её предотвращения является применение std::weak_ptr.

Идея способа: лямбда, запускающаяся в отдельном потоке, должна захватывать weak_from_this(), а не shared_from_this(). Модифицируем только зомби, и обходимся без масштабного рефакторинга.

SimpleZomby

Было:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         while (shis && shis->_listener && shis->_semaphore) {             shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!

Стало:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         while (auto shis = whis.lock()) {             if (shis->_listener && shis->_semaphore) {                 shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));                 std::this_thread::sleep_for(std::chrono::seconds(1));             }         }     }); } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
N11SimpleZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Код вроде бы удалось починить — зомби корректно останавливается.
Кроме того, семафор стал лишним (хотя на самом деле он и до этого не работал) — функционал информирования потока о необходимости остановки удалось возложить на атомарный счётчик ссылок std::shared_ptr. Единственная польза от семафора — защита от повторного вызова функции runOnce, хотя и её можно переложить, например, на проверку _listener.

После удаления семафора:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         while (auto shis = whis.lock()) {             if (shis->_listener) {                 shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));                 std::this_thread::sleep_for(std::chrono::seconds(1));             }         }     }); } 

Однако при ближайшем рассмотрении становится понятно, что в теле лямбды сильная ссылка удерживается необоснованно долго — всё время выполнения длительной операции sleep_for.

Устраняется как-то так:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         while (auto shis = whis.lock()) {             if (shis->_listener) {                 shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));                 shis.reset();                 std::this_thread::sleep_for(std::chrono::seconds(1));             }         }     }); } 

Или вот так:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         while (true) {             if (auto shis = whis.lock(); shis && shis->_listener) {                 shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             } else {                 break;             }             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } 

Ну что, переходим к следующему примеру?
Как бы не так!
У нас тут гонка.
Проще всего её продемонстрировать с помощью переноса слипа в чувствительное место:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         while (true) {             if (auto shis = whis.lock(); shis && shis->_listener) { /*!!!*/         std::this_thread::sleep_for(std::chrono::seconds(1));                 shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             } else {                 break;             }         }     }); } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
N11SimpleZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Вот что произошло:

main thread zomby thread
вызывает runOnce
захватывает в лямбда-функцию слабую ссылку на экземпляр класса
запускает лямбда-функцию на выполнение в отдельном потоке
пытается взять сильную ссылку на экземпляр класса с проверкой результата; проверка проходит успешно
уничтожает единственную внешнюю сильную ссылку
передаёт сообщение полю данных экземпляра класса — listener’у

Зомби остановился.
Но всё ещё слишком поздно — от уничтоженного зомби пришло одно сообщение.
В зависимости от применения, такое поведение может как являться приемлемым, так и послужить триггером для какой-либо неожиданной цепочки действий.

Почему приход данных в listener может быть неожиданным?
Зомби вряд ли является единственным игроком на поле.
Данные, прихода которых он ожидает, наверняка предполагают дальнейшую обработку (расшифровку, парсинг, отображение на экране и т.п.). Уничтожение бизнес-логикой зомби может означать, что механизмы обработки данных такого типа также уничтожены. Передача данных на обработку в уничтоженные механизмы грозит неопределённым поведением.
Также сообщение от зомби может предполагать высокоуровневую ответную реакцию. Вряд ли пользователь обрадуется сообщению «Выполнено успешно» после нажатия кнопки «Отмена».

SteppingZomby

Было:

SteppingZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         if (shis && shis->_listener && shis->_semaphore) {             shis->resolveDnsName();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->connectTcp();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->establishSsl();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->sendHttpRequest();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->readHttpReply();         }     }); } 

Вывод в консоль

N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::establishSsl started
N13SteppingZomby5ZombyE::establishSsl finished
N13SteppingZomby5ZombyE::sendHttpRequest started
N13SteppingZomby5ZombyE::sendHttpRequest finished
N13SteppingZomby5ZombyE::readHttpReply started
N13SteppingZomby5ZombyE::readHttpReply finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Стало (ненужную проверку семафора сразу убираем):

SteppingZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()](){         if (auto shis = whis.lock(); shis && shis->_listener) {             shis->resolveDnsName();         }         if (auto shis = whis.lock(); shis && shis->_listener) {             shis->connectTcp();         }         if (auto shis = whis.lock(); shis && shis->_listener) {             shis->establishSsl();         }         if (auto shis = whis.lock(); shis && shis->_listener) {             shis->sendHttpRequest();         }         if (auto shis = whis.lock(); shis && shis->_listener) {             shis->readHttpReply();         }     }); }

Вывод в консоль

N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Точно так же, как в примере SimpleZomby:
вроде бы удалось остановить зомби;
— сильная ссылка удерживается слишком долго — функции resolveDnsName, connectTcp, establishSsl, sendHttpRequest, readHttpReply требуют валидного состояния экземпляра класса;
— присутствует состояние гонки.
Начатый шаг всегда отрабатывается до конца и всегда отправляет результат в listener, но цепочка шагов в большинстве случаев может быть прервана вышестоящей бизнес-логикой — кроме случая гонки, из-за которой следующий шаг может всё же начаться после уничтожения внешней сильной ссылки.
Рефакторинг private-методов класса может снизить время удержания сильной ссылки, но не позволит полностью устранить состояние гонки.

BoozdedZomby

Было:

BoozdedZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()]() {         while (shis && shis->_semaphore && shis->_listener) {             auto handler = [shis](auto errorCode) {                 if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) {                     std::ostringstream buf;                     buf << "BoozdedZomby has got a fresh data: ";                     for (auto const &elem; : shis->_buffer)                         buf << elem << ' ';                     buf << std::endl;                      shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));                 }             };             shis->_buffer.clear();             shis->_context.async_read(shis->_stream, shis->_buffer, handler);             shis->_context.run();         }     }); } 

Вывод в консоль

BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006

Стало:

BoozdedZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([whis = weak_from_this()]() {         while (auto shis = whis.lock()) {             if (shis->_semaphore && shis->_listener) {                 auto handler = [whis](auto errorCode) {                     auto shis = whis.lock();                     if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) {                         std::ostringstream buf;                         buf << "BoozdedZomby has got a fresh data: ";                         for (auto const &elem; : shis->_buffer)                             buf << elem << ' ';                         buf << std::endl;                          shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));                     }                 };                 shis->_buffer.clear();                 shis->_context.async_read(shis->_stream, shis->_buffer, handler);                 shis->_context.run();             }         }     }); } 

Вывод в консоль

BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
N12BoozdedZomby5ZombyE::~Zomby
N5boozd5azzio10io_contextE::~io_context
N5boozd5azzio13random_streamE::~random_stream
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Зомби-поток отдаёт управление функции boozd::azzio::io_context::run(). При этом необходимо обеспечить валидность источника данных, буфера и коллбэк-функции до момента возврата из run(). Коллбэк-функция будет вызвана при валидном экземпляре класса — а значит, она сможет воспользоваться listener’ом.
В коде присутствуют те же самые недостатки, только выражены они ещё сильнее: BoozdedZomby дорабатывает начатую операцию до конца, и внешний код может лишь отменить очередной повтор. В данном случае вполне можно заявить, что способ не сработал. Останавливать надо текущую операцию, а не следующую.

Вывод по weak-способу

Модификация только зомби только в части замены захвата shared_from_this на захват weak_from_this:
— повышает риски обращения к инвалидированному состоянию (примеры приводить не буду, но на практике встречал);
— не способна устранить состояние гонки (взаимный порядок операций в двух потоках влияет на то, будут ли данные переданы в listener после уничтожения зомби бизнес-логикой).

Функционирование зомби требует захвата сильной ссылки хотя бы на короткое время — и в это время уничтожение всех внешних ссылок не приводит к его остановке. В некоторых случаях сильная ссылка удерживается настолько долго, что делает способ неприменимым на практике.

Валидность объекта вряд ли подходит на роль единственного условия продолжения работы.

В то же время, следует отметить, что проблема гонки связана с выбором способа передачи данных от зомби и может растворяться при выборе более подходящего способа передачи или при наличии более терпимого получателя (ещё вернёмся к этому вопросу).

Способ честный, или use_count

Идея способа: по-прежнему захватываем shared_from_this, но при этом проверяем std::shared_ptr::use_count().

Почему честный?
Использование weak_from_this создаёт иллюзию отказа от влияния на собственное время жизни. А в данном варианте — никакого самообмана, экземпляр класса по-прежнему управляет собственным временем жизни. Но при этом детектирует ситуацию отсутствия внешних сильных ссылок.

SimpleZomby

Было:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         while (shis && shis->_listener && shis->_semaphore) {             shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!

Стало:

SimpleZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         while (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
N11SimpleZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Свойства данной модификации точно совпадают со свойствами weak-способа:
— семафор стал лишним в условии выхода;
— зомби удалось остановить, но присутствует состояние гонки (если проверка use_count() в зомби-потоке прошла успешно — данные будут отправлены даже после уничтожения последней внешней ссылки).

SteppingZomby

Было:

SteppingZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         if (shis && shis->_listener && shis->_semaphore) {             shis->resolveDnsName();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->connectTcp();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->establishSsl();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->sendHttpRequest();         }         if (shis && shis->_listener && shis->_semaphore) {             shis->readHttpReply();         }     }); } 

Вывод в консоль

N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::establishSsl started
N13SteppingZomby5ZombyE::establishSsl finished
N13SteppingZomby5ZombyE::sendHttpRequest started
N13SteppingZomby5ZombyE::sendHttpRequest finished
N13SteppingZomby5ZombyE::readHttpReply started
N13SteppingZomby5ZombyE::readHttpReply finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Стало:

SteppingZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         if (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->resolveDnsName();         }         if (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->connectTcp();         }         if (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->establishSsl();         }         if (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->sendHttpRequest();         }         if (shis && shis.use_count() > 1 && shis->_listener && shis->_semaphore) {             shis->readHttpReply();         }     }); } 

Вывод в консоль

N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Всё точно как в weak-способе.

BoozdedZomby

Было:

BoozdedZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()]() {         while (shis && shis->_semaphore && shis->_listener) {             auto handler = [shis](auto errorCode) {                 if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) {                     std::ostringstream buf;                     buf << "BoozdedZomby has got a fresh data: ";                     for (auto const &elem; : shis->_buffer)                         buf << elem << ' ';                     buf << std::endl;                      shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));                 }             };             shis->_buffer.clear();             shis->_context.async_read(shis->_stream, shis->_buffer, handler);             shis->_context.run();         }     }); } 

Вывод в консоль

BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006

Стало:

BoozdedZomby::Zomby::runOnce

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()]() {         while (shis && shis.use_count() > 1 && shis->_semaphore && shis->_listener) {             auto handler = [&shis;](auto errorCode) {                 if (shis && shis.use_count() > 1 && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) {                     std::ostringstream buf;                     buf << "BoozdedZomby has got a fresh data: ";                     for (auto const &elem; : shis->_buffer)                         buf << elem << ' ';                     buf << std::endl;                      shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));                 }             };             shis->_buffer.clear();             shis->_context.async_read(shis->_stream, shis->_buffer, handler);             shis->_context.run();         }     }); } 

Вывод в консоль

BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
N12BoozdedZomby5ZombyE::~Zomby
N5boozd5azzio10io_contextE::~io_context
N5boozd5azzio13random_streamE::~random_stream
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Вот тут уже значительно лучше, чем в weak-способе: валидность экземпляра класса не считается достаточным основанием для передачи данных в listener, требуется ещё и наличие хотя бы одной внешней сильной ссылки.
При реализации данного способа надо проявлять повышенную внимательность: очень просто ошибочно создать дополнительные сильные копии внутри зомби (обратите внимание — сейчас shis захватывается в коллбэк-функцию по ссылке, а не по значению). Также следует правильно выбирать места для проверок.
Состояние гонки по-прежнему присутствует.

Вывод по use_count-способу

Добавление в условие выхода проверки use_count при сохранении захвата shared_from_this снижает риск обращения к инвалидированному состоянию по сравнению с weak-способом. При этом по-прежнему сохраняется риск передачи данных в listener после уничтожения зомби вышестоящей бизнес-логикой.

Способы use_count и weak пригодны для применения на практике при соответствующей адаптации способа передачи данных.

Такими адаптациями могут быть:
— предоставление зомби прокси-listener’a, обеспечивающего надёжный разрыв соединения с конечным listener’ом (но не обольщайтесь — этот способ не является ни простым, ни изящным; пример можно посмотреть в репозитории, ветка fixes/weak_with_ProxyListener);
— использование сложных систем типа Qt Signals & Slots (однако тут легко получить иллюзию безопасности вместо безопасности);
— отказ от event-driven в пользу поллинга — в этом случае инициатива всегда принадлежит вышестоящему уровню, и возможность обращения к нижестоящему объекту естественным образом исчезает после уничтожения ссылки на него.

Способ синхронный, или this

Идея способа: отказываемся от наследования от std::shared_from_this. Никаких грязных трюков с управлением собственным временем жизни. Никаких узлов в коде.

Лямбда-функция захватывает this. Время жизни потока синхронизируется со временем жизни экземпляра класса. В деструкторе вместо std::thread::detach() вызывается std::thread::join().
Зомби растворяется.
Вместо него возникает псевдо-асинхронный класс: все его методы являются неблокирующими, кроме деструктора. Уничтожение может длиться неопределённо долго — порядка длительности того процесса, которому мы пытались придать свойство асинхронности. На практике это могут быть единицы и даже десятки секунд, и возможна зависимость от внешних факторов (размера ответа на HTTP-запрос, потери сетевого пакета и т.п.).

Было:

SimpleZomby.h

#pragma once  #include <memory> #include <atomic> #include <thread>  #include "Common/Manager.h"  namespace Common { class Listener; } // namespace Common  namespace SimpleZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public:     static std::shared_ptr<Zomby> create();      ~Zomby() override;      void runOnce(std::shared_ptr<Common::Listener> listener) override;  private:     Zomby();      using Semaphore = std::atomic<bool>;      std::shared_ptr<Common::Listener> _listener;     Semaphore _semaphore = false;     std::thread _thread; }; } // namespace SimpleZomby 

SimpleZomby.cpp

#include <sstream>  #include "SimpleZomby.h" #include "Common/Listener.h"  namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() {     return std::shared_ptr<Zomby>(new Zomby()); }  Zomby::Zomby() = default;  Zomby::~Zomby() {     _semaphore = false;      if (_thread.joinable()) {         _thread.detach();     }      if (_listener) {         std::ostringstream buf;         buf << typeid(*this).name() << "::" << __func__ << std::endl;         _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));     } }  void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         while (shis && shis->_listener && shis->_semaphore) {             shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } } // namespace SimpleZomby 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!

Стало:

SimpleZomby.h

#pragma once  #include <memory> #include <atomic> #include <thread>  #include "Common/Manager.h"  namespace Common { class Listener; } // namespace Common  namespace SimpleZomby { class Zomby final : public Common::Manager { public:     static std::shared_ptr<Zomby> create();      ~Zomby() override;      void runOnce(std::shared_ptr<Common::Listener> listener) override;  private:     Zomby();      using Semaphore = std::atomic<bool>;      std::shared_ptr<Common::Listener> _listener;     Semaphore _semaphore = false;     std::thread _thread; }; } // namespace SimpleZomby 

SimpleZomby.cpp

#include <sstream>  #include "SimpleZomby.h" #include "Common/Listener.h"  namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() {     return std::shared_ptr<Zomby>(new Zomby()); }  Zomby::Zomby() = default;  Zomby::~Zomby() {     _semaphore = false;      if (_thread.joinable()) {         _thread.join();     }      if (_listener) {         std::ostringstream buf;         buf << typeid(*this).name() << "::" << __func__ << std::endl;         _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));     } }  void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([this](){         while (_listener && _semaphore) {             _listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } } // namespace SimpleZomby 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
N11SimpleZomby5ZombyE::~Zomby
============================================================
| Zomby was killed |
============================================================
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Отметим:
— это пока единственный пример, в котором деструктор зомби (хотя это уже и не зомби на самом деле) был вызван до вывода «Zomby was killed»;
— семафор наконец-то сыграл ту роль, которая отводилась ему изначально, хотя его можно заменить проверкой валидности _listener;
— требование хранения экземпляра класса исключительно в std::shared_ptr стало излишним, статическую функцию create() можно упразднить в пользу выноса конструктора в public-секцию класса;
— захват this в лямбда-функцию требует постоянного расположения экземпляра класса в памяти — унаследованный от Common::Manager запрет copy- и move-семантики по-прежнему важен.

Модификации SteppingZomby и BoozdedZomby в данном случае полностью аналогичны, приводить их в статье нет смысла. Кому интересно — смотрите в ветке fixes/this репозитория.

Вывод по this-способу

Способ хорош всем, кроме блокирующего деструктора. Блокировка GUI-потока на несколько секунд вполне может заставить операционную систему считать процесс зависшим. В таком виде способ вряд ли пригоден для практического применения.

Способ пригодный, или semaphore_done_right

Во всех рассмотренных до сих пор примерах класс использовался как агрегатор данных, необходимых для работы отдельного потока. Это приводило к необходимости обеспечения времени жизни экземпляра класса, превышающего время жизни запущенного потока.
Да, агрегировать данные в классе удобно.
Но только до тех пор, пока это не приводит к букету сложных проблем.

Идея способа: правильное разграничение ответственностей между классом и потоком.
Обязанности класса:
— запуск потока;
— перевод семафора в запрещающее состояние в деструкторе.
Обязанности потока:
— не продолжать работу при запрещающем состоянии семафора.
Никакой связи по данным (кроме разделяемого семафора), никаких требований по времени жизни (особенно от нижележащего потока в сторону вышележащего класса). Никакого захвата экземпляра класса в лямбду — ни в виде сырого указателя, ни в виде умного.
Для устранения гонки, возникаюшей в способах weak и use_count, придётся использовать синхронизацию с помощью мьютекса. Деструктор захватывает мьютекс на время смены значения семафора, а отдельный поток — на время выполнения операции передачи данных в listener.
Деструктор вызывает std::thread::detach(), не дожидаясь завершения потока (вернёмся к этому вопросу позднее).

Было:

SimpleZomby.h

#pragma once  #include <memory> #include <atomic> #include <thread>  #include "Common/Manager.h"  namespace Common { class Listener; } // namespace Common  namespace SimpleZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public:     static std::shared_ptr<Zomby> create();      ~Zomby() override;      void runOnce(std::shared_ptr<Common::Listener> listener) override;  private:     Zomby();      using Semaphore = std::atomic<bool>;      std::shared_ptr<Common::Listener> _listener;     Semaphore _semaphore = false;     std::thread _thread; }; } // namespace SimpleZomby 

SimpleZomby.cpp

#include <sstream>  #include "SimpleZomby.h" #include "Common/Listener.h"  namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() {     return std::shared_ptr<Zomby>(new Zomby()); }  Zomby::Zomby() = default;  Zomby::~Zomby() {     _semaphore = false;      if (_thread.joinable()) {         _thread.detach();     }      if (_listener) {         std::ostringstream buf;         buf << typeid(*this).name() << "::" << __func__ << std::endl;         _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));     } }  void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     _semaphore = true;      _thread = std::thread([shis = shared_from_this()](){         while (shis && shis->_listener && shis->_semaphore) {             shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } } // namespace SimpleZomby 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!

Стало:

SimpleZomby.h

#pragma once  #include <memory> #include <atomic> #include <thread>  #include "Common/Manager.h"  namespace Common { class Listener; } // namespace Common  namespace SimpleZomby { class Zomby final : public Common::Manager { public:     static std::shared_ptr<Zomby> create();      ~Zomby() override;      void runOnce(std::shared_ptr<Common::Listener> listener) override;  private:     Zomby();      using Semaphore = std::pair<std::mutex, bool>;      std::shared_ptr<Common::Listener> _listener;     std::shared_ptr<Semaphore> _semaphore;     std::thread _thread; }; } // namespace SimpleZomby 

SimpleZomby.cpp

#include <sstream>  #include "SimpleZomby.h" #include "Common/Listener.h"  namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() {     return std::shared_ptr<Zomby>(new Zomby()); }  Zomby::Zomby() = default;  Zomby::~Zomby() {     if (_semaphore) {         if (std::this_thread::get_id() != _thread.get_id()) {             auto guard = std::lock_guard(_semaphore->first);             _semaphore->second = false;         } else {             _semaphore->second = false;         }     }      if (_thread.joinable()) {         _thread.detach();     }      if (_listener) {         std::ostringstream buf;         buf << typeid(*this).name() << "::" << __func__ << std::endl;         _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));     } }  void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     // two separate lines needed because of absence of std::mutex move c-tor     _semaphore = std::make_shared<Semaphore>();     _semaphore->second = true;      _thread = std::thread([listener, semaphore = _semaphore](){         while (listener && semaphore) {             {                 auto guard = std::lock_guard(semaphore->first);                 if (!semaphore->second) {                     break;                 }                 listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             }             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } } // namespace SimpleZomby 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
N11SimpleZomby5ZombyE::~Zomby
============================================================
| Zomby was killed |
============================================================
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Модификации для SteppingZomby и BoozdedZomby аналогичны (см. ветку fixes/semaphore_done_right) — настолько аналогичны, что объявление класса BoozdedZomby совпало с объявлением класса SimpleZomby, а в SteppingZomby присутствуют лишь допонительные статические функции, которые могут быть преобразованы в свободные функции.

Свойства данного способа:
— время выполнения деструктора перестало зависеть от внешних факторов — осталась только конкуренция за мьютекс, защищающий потенциально очень быструю операцию передачи данных в обработку;
— аккуратная работа с мьютексом позволила избавиться от состояния гонки — корректно написанная лямбда-функция гарантированно не отправит данные в listener после завершения деструктора;
— предусмотрена защита от deadlock’а на случай вызова деструктора из обработчика события прихода данных (уничтожение соединения в ответ на получение данных — вполне корректный сценарий);
— требование хранения экземпляра класса исключительно в std::shared_ptr стало излишним, статическую функцию create() можно упразднить в пользу выноса конструктора в public-секцию класса;
— запрет copy- и move-семантики, унаследованный от Common::Manager, стал излишним;
— listener по-прежнему хранится в поле класса — но только для диагностического вывода в деструкторе, он не связывает экземпляр класса с потоком выполнения;
— класс перестал зависеть от специфики данных, необходимых для работы отдельного потока, что открывает дополнительные возможности для обобщения;
— поток может продолжить выполнение в detached-состоянии после уничтожения экземпляра класса.

Последний пункт является потенциально проблемным по следующим причинам:
— маскирует ошибки в условии выхода из функции, выполняющейся в отдельном потоке;
— отдельный поток может дожить до завершения главного потока — тогда он будет аварийно зачищен системой, что приведёт к сбою деинициализации.

Способ финальный, или semaphore_done_right_with_ThreadJoiner

Если detach() потока может создавать проблемы — попробуем что-нибудь с этим сделать (запустим новый поток, который будет асинхронно ждать завершения старого потока).

Идея способа:
— нужна новая сущность (ThreadJoiner), способная взять на себя ответственность за отработавший поток (пожалуй, более подходящее названием для такой сущности — ThreadSanitizer, но оно уже занято);
— ThreadJoiner в отдельном служебном потоке будет ожидать отработки std::thread::join() последовательно для всех потоков, ответственность за которые он принял;
— деструктор ThreadJoiner будет блокировать выполнение до тех пор, пока не завершатся все потоки, ответственность за которые он принял.

Абстрактный интерфейс:

ThreadJoiner.h

#pragma once  #include <thread>  namespace Common { class ThreadJoiner { public:     ThreadJoiner() = default;     ThreadJoiner(const ThreadJoiner&) = delete;     ThreadJoiner(ThreadJoiner&&) = delete;     ThreadJoiner& operator=(const ThreadJoiner&) = delete;     ThreadJoiner& operator=(ThreadJoiner&&) = delete;      virtual ~ThreadJoiner() = default;      virtual void join(std::thread&&) = 0; }; } // namespace Common 

Конкретная асинхронная реализация:

ThreadJoinerAsync.h

#pragma once  #include <thread> #include <queue> #include <mutex> #include <optional> #include <condition_variable>  #include "Common/ThreadJoiner.h"  namespace Common { class ThreadJoinerAsync final : public ThreadJoiner { public:     ThreadJoinerAsync();     ThreadJoinerAsync(const ThreadJoinerAsync&) = delete;     ThreadJoinerAsync(ThreadJoinerAsync&&) = delete;     ThreadJoinerAsync& operator=(const ThreadJoinerAsync&) = delete;     ThreadJoinerAsync& operator=(ThreadJoinerAsync&&) = delete;      ~ThreadJoinerAsync() override;      void join(std::thread&&) override;  private:     using Data = std::queue<std::thread>;     using Semaphore = std::atomic<bool>;      std::mutex _mutex;     Data _data;     Semaphore _semaphore;     std::condition_variable _cv;     std::thread _thread;      void threadFn(); }; } // namespace Common 

ThreadJoinerAsync.cpp

#include "ThreadJoinerAsync.h"  namespace Common { ThreadJoinerAsync::ThreadJoinerAsync()     : _semaphore(true)     , _thread(&ThreadJoinerAsync::threadFn, this) { }  ThreadJoinerAsync::~ThreadJoinerAsync() {     _semaphore = false;     _cv.notify_one();      if (_thread.joinable()) {         _thread.join();     } }  void ThreadJoinerAsync::join(std::thread&& thread) {     {         auto lock = std::lock_guard(_mutex);         _data.push(std::move(thread));     }      _cv.notify_one(); }  void ThreadJoinerAsync::threadFn() {     while (true) {         auto lock = std::unique_lock(_mutex);         _cv.wait(lock, [this](){ return !_semaphore || !_data.empty(); });          if (!_semaphore && _data.empty()) {             return;         }          auto threadToJoin = std::move(_data.front());         _data.pop();          lock.unlock();          if (threadToJoin.joinable()) {             threadToJoin.join();         }     } } } // namespace Common 

Применение ThreadJoiner в SimpleZomby:

SimpleZomby.h

#pragma once  #include <memory> #include <atomic> #include <thread>  #include "Common/Manager.h"  namespace Common { class Listener; class ThreadJoiner; } // namespace Common  namespace SimpleZomby { class Zomby final : public Common::Manager { public:     Zomby(std::shared_ptr<Common::ThreadJoiner>);      ~Zomby() override;      void runOnce(std::shared_ptr<Common::Listener> listener) override;  private:      using Semaphore = std::pair<std::mutex, bool>;      std::shared_ptr<Common::Listener> _listener;     std::shared_ptr<Semaphore> _semaphore;     std::thread _thread;     const std::shared_ptr<Common::ThreadJoiner> _joiner; }; } // namespace SimpleZomby 

SimpleZomby.cpp

#include <sstream>  #include "SimpleZomby.h" #include "Common/Listener.h" #include "Common/ThreadJoiner.h"  namespace SimpleZomby { Zomby::Zomby(std::shared_ptr<Common::ThreadJoiner> joiner)     : _joiner(joiner) {     if (!_joiner) {         throw std::runtime_error("An empty joiner in SimpleZomby::Zomby::Zomby");     } }  Zomby::~Zomby() {     if (_semaphore) {         if (std::this_thread::get_id() != _thread.get_id()) {             auto guard = std::lock_guard(_semaphore->first);             _semaphore->second = false;         } else {             _semaphore->second = false;         }     }      if (_thread.joinable()) {         _joiner->join(std::move(_thread));     }      if (_listener) {         std::ostringstream buf;         buf << typeid(*this).name() << "::" << __func__ << std::endl;         _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));     } }  void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) {     if (_semaphore) {         throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");     }      _listener = listener;     // two separate lines needed because of absence of std::mutex move c-tor     _semaphore = std::make_shared<Semaphore>();     _semaphore->second = true;      _thread = std::thread([listener, semaphore = _semaphore](){         while (listener && semaphore) {             {                 auto guard = std::lock_guard(semaphore->first);                 if (!semaphore->second) {                     break;                 }                 listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));             }             std::this_thread::sleep_for(std::chrono::seconds(1));         }     }); } } // namespace SimpleZomby 

main.cpp

#include <chrono> #include <thread> #include <sstream>  #include "Common/Impl/WriteToConsoleListener.h" #include "Common/Impl/ThreadJoinerAsync.h" #include "SimpleZomby/SimpleZomby.h"  int main() {     auto writeToConsoleListener = Common::WriteToConsoleListener::instance();     auto joiner = std::make_shared<Common::ThreadJoinerAsync>();      {         auto simpleZomby = SimpleZomby::Zomby(joiner);         simpleZomby.runOnce(writeToConsoleListener);          std::this_thread::sleep_for(std::chrono::milliseconds(4500));     } // Zomby should be killed here      {         std::ostringstream buf;         buf << "============================================================\n"             << "|                      Zomby was killed                    |\n"             << "============================================================\n";         if (writeToConsoleListener) {             writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str()));         }     }      std::this_thread::sleep_for(std::chrono::milliseconds(5000));      return 0; } 

Вывод в консоль

SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
N11SimpleZomby5ZombyE::~Zomby
============================================================
| Zomby was killed |
============================================================
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener

Предупреждение: код ThreadJoiner тестировался поверхностно; задумаете втащить его в production — я не виноват.

Преимущества способа:
— гарантированное корректное завершение всех потоков, ответственность за которые была возложена на ThreadJoiner;
— гарантированное обнаружение ошибки в условии выхода из отдельного потока, приводящей к бесконечному выполнению — главный поток зависнет при завершении;
— ThreadJoiner может быть дополнен диагностическими сообщениями, упрощающими выявление менее критичных ошибок в условии выхода из отдельного потока — тех, которые приводят к некоторому конечному излишнему времени выполнения потока.

Модификации SteppingZomby и BoozdedZomby — полностью аналогичны (см. ветку fixes/semaphore_done_right_with_ThreadJoiner).

Пара слов об официальных примерах boost

В первой части статьи приведены два примера из официальной документации boost:
Пример HTTP-клиента
Пример Websocket-клиента
Вызов ioc.run() в main() блокирует управление до наступления одного из событий:
— успешная отработка всей цепочки;
— ошибка.
Досрочное уничтожение вышестоящей бизнес-логикой экземпляра класса session в представленном коде невозоможно. Это само по себе гарантирует достаточное время жизни данных, агрегированных в полях класса session. Судя по всему, представленный код будет успешно работать даже при модификации его this-способом. Использование shared_from_this в данном случае избыточно, достаточно захвата сырого указателя this.

Откуда же тогда взялся shared_from_this?
Вероятно, из более реального кода, в котором были предусмотрены сценарии остановки соединения путём его уничтожения. И конечно, отсутствие контроля собственного времени жизни приводило к неопределённому поведению (читай «к крэшам»). Крэши разменяли на зомби, а зомби подарили копипастерам.

Вывод

Решения, приведённые в статье, специфичны. Они учитывают особенности изначального кода — в том числе способ передачи результатов работы асинхронного процесса.

Однако в результате разбора примеров стало понятно, какие архитектурные решения привели к проблемам:
— нарушение принципа single responsibility — на класс были возложены как функция информирования асинхронно выполняющегося процесса о необходимости остановки, так и функция хранения данных для этого процесса;
— отсюда возникло требование от нижележащей сущности к вышележащей — асинхронный процесс нуждался в гарантии валидности экземпляра класса в течение определённого времени;
— отсюда возникла необходимость контроля собственного времени жизни с помощью техники std::enable_shared_from_this.

Наиболее корректный код удалось получить без применения std::enable_shared_from_this.

Чего и Вам желаю.

ссылка на оригинал статьи https://habr.com/ru/post/478190/