Вызов деструктора 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 шт):

Автор решения: Swift - Friday Pie

Ситуация "работает как должно" для последней копии ссылки на объект в 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, ...

→ Ссылка