Определите последствие исключения, покинувшего функцию потока, и способ безопасно обработать его. Код долже...

Определите последствие исключения, покинувшего функцию потока, и способ безопасно обработать его. Код должен завершиться без аварийного завершения процесса.

#include <stdexcept>
#include <thread>

void worker() {
    throw std::runtime_error("ошибка");
}

int main() {
    std::thread t(worker);
    t.join();
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если исключение покидает функцию, выполняемую потоком std::thread, оно не передаётся в поток, вызвавший join(). Вызов приводит к std::terminate, поэтому исключение нужно перехватить внутри рабочего потока либо явно передать через std::exception_ptr, std::promise или другой канал обмена.

Исторический контекст

Модель исключений C++ изначально рассчитана на раскрутку стека внутри одного потока исполнения. При появлении std::thread стандарт не стал неявно переносить исключения между потоками: это потребовало бы определить момент и контекст их повторного выброса.

Поэтому потоковая библиотека разделяет выполнение и передачу ошибок. std::thread отвечает за управление потоком, а std::future и std::promise предоставляют отдельный механизм передачи результата или исключения.

Постановка проблемы

В примере исключение покидает worker. Объект std::thread не перехватывает его для вызывающего потока, поэтому программа вызывает std::terminate, обычно завершая процесс до нормального продолжения main.

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

Подробное решение

Самый простой вариант — перехватить исключение внутри функции потока:

#include <exception> #include <iostream> #include <thread> void worker() { try { throw std::runtime_error("ошибка"); } catch (const std::exception& e) { std::cerr << e.what() << ' '; } } int main() { std::thread t(worker); t.join(); }

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

Для передачи исключения в управляющий поток обычно применяют std::promise и std::future. Исключение сохраняется вызовом promise.set_exception(std::current_exception()), а затем повторно выбрасывается в future.get().

Важно различать два случая: исключение внутри функции потока и исключение при создании самого std::thread. Если конструктор потока не смог создать поток, он может выбросить исключение непосредственно в вызывающем потоке; это отдельный сценарий.

Даже при корректной обработке исключений объект std::thread должен быть присоединён через join() или отсоединён через detach() до уничтожения. Иначе уничтожение всё ещё joinable-объекта также вызывает std::terminate.

Ситуация из практики

Сервис запускает рабочий поток для обработки задания. Вариант с отсутствием перехвата прост, но опасен: единичная ошибка завершает весь процесс. Перехват только в main через try/catch не помогает, поскольку исключение не пересекает границу потока.

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

Практичным решением является std::future: рабочая функция оборачивается в задачу, исключение автоматически сохраняется, а управляющий код вызывает get() в ожидаемом месте. Это даёт явную передачу ошибки и сохраняет корректное управление временем жизни потока.

Что кандидаты часто упускают

  1. Вопрос: Можно ли обернуть t.join() в try/catch, чтобы поймать исключение из worker?

    Ответ: Нет. join() ожидает завершения потока, но не переносит исключение из его стека в вызывающий поток. try/catch вокруг join() поймает только исключения, непосредственно возникшие в вызывающем потоке, например ошибку самого вызова join().

  2. Вопрос: Что произойдёт, если исключение перехвачено внутри потока, но объект std::thread не присоединён?

    Ответ: Перехват исключения не меняет состояние объекта std::thread. Если к моменту его уничтожения joinable() возвращает true, деструктор вызовет std::terminate. Поэтому обработка ошибки и корректное завершение жизненного цикла потока — независимые обязанности.

  3. Вопрос: Почему std::future::get() может выбросить исключение, хотя исключение возникло в другом потоке?

    Ответ: std::future хранит состояние общей асинхронной операции. При возникновении исключения обёртка задачи сохраняет исключение в этом состоянии, а get() извлекает результат или повторно выбрасывает сохранённое исключение в потоке, который вызвал get(). Это явный механизм передачи, в отличие от обычного std::thread.