Определите последствие исключения, покинувшего функцию потока, и способ безопасно обработать его. Код должен завершиться без аварийного завершения процесса.
#include <stdexcept>
#include <thread>
void worker() {
throw std::runtime_error("ошибка");
}
int main() {
std::thread t(worker);
t.join();
}
Если исключение покидает функцию, выполняемую потоком 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() выбросит исключение из рабочего потока. Если ошибка должна быть обработана в управляющем потоке, её нужно сохранить там, где она возникла, и затем явно передать.
Самый простой вариант — перехватить исключение внутри функции потока:
Перехват должен охватывать весь код рабочего потока, включая вызываемые им функции. После перехвата поток может записать ошибку в защищённое хранилище, установить флаг остановки или выполнить локальное восстановление.
Для передачи исключения в управляющий поток обычно применяют 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() в ожидаемом месте. Это даёт явную передачу ошибки и сохраняет корректное управление временем жизни потока.
Вопрос: Можно ли обернуть t.join() в try/catch, чтобы поймать исключение из worker?
Ответ: Нет. join() ожидает завершения потока, но не переносит исключение из его стека в вызывающий поток. try/catch вокруг join() поймает только исключения, непосредственно возникшие в вызывающем потоке, например ошибку самого вызова join().
Вопрос: Что произойдёт, если исключение перехвачено внутри потока, но объект std::thread не присоединён?
Ответ: Перехват исключения не меняет состояние объекта std::thread. Если к моменту его уничтожения joinable() возвращает true, деструктор вызовет std::terminate. Поэтому обработка ошибки и корректное завершение жизненного цикла потока — независимые обязанности.
Вопрос: Почему std::future::get() может выбросить исключение, хотя исключение возникло в другом потоке?
Ответ: std::future хранит состояние общей асинхронной операции. При возникновении исключения обёртка задачи сохраняет исключение в этом состоянии, а get() извлекает результат или повторно выбрасывает сохранённое исключение в потоке, который вызвал get(). Это явный механизм передачи, в отличие от обычного std::thread.