Вызов деструктора std::future при выбросе исключения
Есть такой код:
#include <chrono>
#include <future>
#include <iostream>
using namespace std::chrono_literals;
int main()
{
try {
std::atomic<bool> stopTask;
stopTask.store(false, std::memory_order_seq_cst);
auto future = std::async([&stopTask]() {
for (int i = 0; i < 20; ++i)
{
if (stopTask.load(std::memory_order_seq_cst))
break;
std::this_thread::sleep_for(500ms); // Imitation of useful task.
}
});
// Some useful work in the main thread.
throw std::runtime_error("Error"); // Oops! Something went wrong in the main thread.
// Normal shutdown.
stopTask.store(true, std::memory_order_seq_cst);
future.get();
}
catch (...)
{
std::cout << "Exception caught" << std::endl;
}
}
Я запускаю долго выполняющуюся задачу в отдельном потоке с помощью std::async и получаю на неё std::future. И после этого в главном потоке происходит исключение. Начинается раскрутка стека и вызываются деструкторы. Как только дело доходит до деструктора std::future раскрутка стека останавливается и главный поток блокируется пока не завершится второй поток.
Такое поведение вызывает у меня ощущение, что что-то идёт не так. Вроде как я встречал рекомендации делать деструкторы как можно более быстрыми и не выполнять в них долгие операции. А тут получается деструктор очень медленный и раскрутка стека происходит очень долго.
Вопрос: это правильно, что раскрутка стека происходит так долго? Может быть я что-то делаю не так и на самом деле нужно делать по другому? Какие есть best practices в данных случаях?
В данном конкретном коде деструктор std::future можно ускорить, если сделать класс обёртку вокруг std::atomic stopTask наподобие RAII, который будет устанавливать stopTask в true в деструкторе. Но всё равно на выполнение деструктора может уйти до 500 мс. И это уже не ускорить, так как в моём приложении реальная минимальная операция занимает столько времени.
Ответы (1 шт):
Ситуация "работает как должно" для последней копии ссылки на объект в future. Возможные решения - не ждать уничтожения future и дать задачам завершиться , либо использовать раскрутку стека чтобы их остановить. Коряво, но наглядно:
std::atomic<bool> stopTask;
stopTask.store(false, std::memory_order_seq_cst);
auto future = std::async([&stopTask]() {
for (int i = 0; i < 20; ++i)
{
if (stopTask.load(std::memory_order_seq_cst)) {
std::cout << "Stopped" << std::endl;
break;
}
std::this_thread::sleep_for(500ms); // Imitation of useful task.
}
std::cout << "Finished" << std::endl;
});
struct Stopper {
std::atomic<bool>& stop;
~Stopper() {stop.store(true, std::memory_order_seq_cst);}
} stopper {stopTask};
В отсутствии stopper сообщение об исключении появится только после "Finished" Это защита от memory smash - нелегальной записи в память, которая была занята объектом, завершившим свое существование.
В последнем случае всегда существует проблем существования "мертвой руки": если постигшая нас катастрофа уничтожила сам механизм управления, гарантировать правильный итог мы не можем.
Не ожидающий подход - поместить future и управление куда-то во вне охраняемого блока:
int main()
{
std::atomic<bool> stopTask;
std::future<void> gFuture;
try {
stopTask.store(false, std::memory_order_seq_cst);
auto future = std::async([&stopTask]() {
for (int i = 0; i < 20; ++i)
{
if (stopTask.load(std::memory_order_seq_cst)) {
std::cout << "Stopped" << std::endl;
break;
}
std::this_thread::sleep_for(500ms); // Imitation of useful task.
}
std::cout << "Finished" << std::endl;
});
gFuture = std::move(future);
// Some useful work in the main thread.
throw std::runtime_error("Error"); // Oops! Something went wrong in the main thread.
// Normal shutdown.
stopTask.store(true, std::memory_order_seq_cst);
future.get();
}
catch (...)
{
stopTask.store(true, std::memory_order_seq_cst);
std::cout << "Exception caught" << std::endl;
}
}
Ну, и как показывает cppreference, можно поменять политику async c помощью вызова std::async( std::launch::deferred, ...