Я пытаюсь понять, как исключения могут распространяться между различными функциями и возвращаться в основную функцию на С++. У меня есть небольшая установка:
основной.cpp:
int run () {
.
.
try {
testException(file);
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
test.cpp
std::exception_ptr g_exceptionPtr = nullptr;
void testThread() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
.
.
}
void testException(const std::string &file) {
TestCSV csv(file);
try {
std::thread input(testThread);
csv.writeToCSV(file, a, b, c);
input.join();
} catch (const std::runtime_error &e) {
g_exceptionPtr = std::current_exception();
std::rethrow_exception(g_exceptionPtr);
}
}
В test_csv.cpp
:
TestCSV::writeToCSV(const std::string &file, const std::string &a, const std::string &b, const std::string &c) {
.
.
std::ofstream outFile(file);
if (!outFile.is_open()) {
throw std::runtime_error("Unable to open file for writing.");
}
}
Теперь я хочу передать ошибку из функции writeToCSV
и обработать ее в main.cpp
. Но в настоящее время это исключение перехватывается test.cpp
, но не выбрасывается повторно в main.cpp
.
В чем проблема и как ее решить?
P.S: Приведенный выше код является всего лишь примером. Если какая-либо информация отсутствует, пожалуйста, дайте мне знать.
Отвечает ли это на ваш вопрос? Будет ли функция main() перехватывать исключения, возникающие из потоков?
Вы устанавливаете g_exceptionPtr
в треде, а затем перезаписываете его в основной ветке.
Примечание: std::cerr имеет модульную буферизацию, т. е. каждый стандартный инструмент вставки потока сбрасывает поток после вывода. Нет необходимости в дополнительном сливе, который делает std::endl
. Используйте '\n'
, чтобы завершить строку, если нет веской причины не делать этого.
@interjay, да, это не нужно. Но если я удалю его, у меня будет такое же поведение
std::rethrow_exception(std::current_exception());
это просто throw;
...
Привет @MarekR, не могли бы вы рассказать мне, как мне использовать std::future
и std::promise
здесь?
@Jarod42 Jarod42, я пробовал использовать throw
, но поведение такое же. Он не распространяется на main.cpp
Я не говорил, что это нужно делать, просто эквивалентность (чтобы также лучше понять, почему это неправильный путь). (в непрерывности try { foo(); } catch(...) { throw;}
— это просто foo();
.
@Preeti то, что вы описываете, невозможно для нормального поведения try
/catch
/throw
, исключение не перехватывается где-либо еще до того, как оно достигнет main
, поэтому здесь явно задействован еще один аспект, который вы плохо объясняете. Пожалуйста, отредактируйте свой вопрос, чтобы предоставить лучший минимально воспроизводимый пример, который действительно демонстрирует реальную проблему в действии.
Если вы запросите трассировку стека, см. en.cppreference.com/w/cpp/utility/basic_stacktrace
Вы вызываете writeToCSV()
в контексте того же потока, который вызывает run()
, поэтому любое исключение, которое writeToCSV()
выбрасывает и не перехватывается, уже будет распространяться на run()
, как и ожидалось. Для этого вам не нужно делать ничего дополнительно. Просто не делайте catch
исключение вообще, а если и сделали, то просто повторно throw
его (не std::rethrow_exception()
). Использование std::thread
в этой ситуации не имеет значения.
int run () {
...
try {
testException(file);
} catch (const std::exception &e) {
// this should catch whatever testException() throws...
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
void testException(const std::string &file) {
TestCSV csv(file);
csv.writeToCSV(file, a, b, c);
// or:
try {
TestCSV csv(file);
csv.writeToCSV(file, a, b, c);
}
catch (const std::exception &e) {
...
throw;
}
}
С другой стороны, если ваш вопрос касается распространения исключений между потоками, вам следует изменить свой тест, чтобы отразить это. Перемещение writeToCSV()
в std::thread
было бы лучшей проверкой std::current_exception
и std::rethrow_exception
.
std::current_exception
позволяет вам перехватить перехваченное исключение, чтобы вы могли получить к нему доступ за пределами catch
. Вы можете зафиксировать исключение в исходном потоке, который его вызвал, переместить его в нужный поток, а затем вызвать std::rethrow_exception
в этом потоке. В этом случае после того, как вы join
ввели std::thread
, если exception_ptr
был назначен, например:
void testThread(const std::string &file, std::exception_ptr &exceptionPtr) {
try {
TestCSV csv(file);
csv.writeToCSV(...);
} catch (const std::exception &) {
exceptionPtr = std::current_exception();
}
}
void testException(const std::string &file) {
std::exception_ptr exceptionPtr = nullptr;
std::thread writer(testThread, file, std::ref(exceptionPtr));
...
writer.join();
if (exceptionPtr)
std::rethrow_exception(exceptionPtr);
}
int run () {
...
try {
testException(file);
} catch (const std::exception &e) {
// this should catch whatever testThread() throws...
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
Привет @Remy Lebeau, csv.writeToCSV(...);
следует вызывать внутри testException
функции. И нет необходимости ставить g_exceptionPtr
в testThread
. Я обновил свой ОП, чтобы удалить это...
@Preeti Дело в том, что все, что выбрасывает testThread
, вам нужно поймать и установить exceptionPtr
так, чтобы оно указывало на current_exception
, что затем можно повторно выбросить после join()
запуска потока.
@Привети, судя по тому, как вы изложили проблему, вообще нет смысла использовать std::current_exception()
и std::rethrow_exception()
. Я обновил свой ответ.
Привет @RemyLebeau, спасибо за ответ. Я хотел добавить одну вещь: testThread
на самом деле ожидает событий нажатия клавиш и, следовательно, вызывает testException
как часть другой логики. Но я также получаю Debug error: abort () has been called
от вашего первого решения
Вы могли бы упомянуть std::promise
и std::future
, у которых есть для этого оборудование.
@Yakk-AdamNevraumont Я мог бы, если бы знал что-нибудь о них. Но я этого не делаю, поэтому я не сделал
@Preeti, это означает, что вы генерируете исключение, которое не перехватывается.
Привет @RemyLebeau, я также пытался добавить throw std::runtime_error("Testing exception.");
в testException
внутри try
блока, но это не выбрасывается повторно. Внутри try
мне нужно вызвать std::thread
, так как этот поток будет искать любые события нажатия клавиш, и это повлияет на то, как функция writeToCSV
вызывается в разных местах... Но когда я добавляю throw std::runtime_error("Testing exception.");
снаружи try
(перед вызовом std::thread
, это исключение перехватывается и повторно выбрасывается в основной. Я думаю, что поток принудительно завершен, и это обрабатывается неправильно...
Один из способов решения этой проблемы — использовать std::future
для достижения результатов.
std::future<T>
— это асимметричный союз между T
и исключением. Когда вы вызываете .get()
, он выдает исключение, если оно содержит исключение.
Есть помощники для работы с std::future
, а именно std::packaged_task<R(Args...)>
и std::promise<T>
.
Предположим, у вас есть пул потоков, который работает с очередью задач.
Во-первых, потокобезопасная конструкция очереди:
template<class T>
struct threadsafe_queue {
T pop();
std::queue<T> pop_all();
template<class Duration>
std::optional<T> wait_or_pop( Duration );
void push(T);
void push_many(std::queue<T>);
private:
auto lock() const { return std::unique_lock{m}; }
mutable std::mutex m;
std::condition_variable cv;
std::deque<T> q;
};
реализация оставлена на усмотрение читателя.
Затем пул потоков:
struct thread_pool {
template<class F>
auto queue_task( F f ) {
using R = std::invoke_result_t<F&>;
std::packaged_task<R()> t = std::forward<F>(f);
std::future<R> f = t.get_future();
q.push( std::packaged_task<void()>( std::move(t) );
return f;
}
void start_thread( std::size_t n=1 );
private:
threadsafe_queue<std::packaged_task<void()>> q;
std::vector<std::thread> threads;
};
теперь вы передаете свои задачи thread_pool
. Их сбивают с толку packaged_task
. Если они выбрасывают исключение, их исключение перемещается в std::future
.
Чтобы распространить исключение, вы просто вызываете .get()
в будущем.
Чтобы перехватывать исключения из каждого потока, сгенерированного из основного, нам нужно создать массив исключений_ptr(s), соответствующий количеству потоков, а затем обработать исключения из каждого потока в основном потоке, как показано ниже.
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>
#include <memory>
#include <sstream>
#include <ctime>
using namespace std;
exception_ptr eptr[10];
void f(int i)
{
try {
auto id = this_thread::get_id();
cout << "Thread " << id << " sleeping ..." << '\n';
this_thread::sleep_for(chrono::seconds(1));
if (i & 1) {
stringstream ss;
ss << id ;
string str ("Exception from " + ss.str());
throw std::runtime_error(str.c_str());
}
}
catch(...)
{
eptr[i] = current_exception();
}
}
int main(int argc, char **argv)
{
thread th[10];
for(int i = 0; i < 10; ++i) {
this_thread::sleep_for(chrono::seconds(1));
th[i] = thread(f,i);
}
for(int i = 0; i < 10; ++i) {
if (th[i].joinable())
th[i].join();
if (eptr[i]) {
try{
std::rethrow_exception(eptr[i]);
}
catch(const std::exception &ex)
{
std::cerr << "Thread exited with exception: " << ex.what() << "\n";
}
}
}
return 0;
}
Пример вывода:
$ ./excep_thread1
Thread 0xa000128e0 sleeping ...
Thread 0xa000229f0 sleeping ...
Thread 0xa00022b10 sleeping ...
Thread 0xa00022c10 sleeping ...
Thread 0xa000233a0 sleeping ...
Thread 0xa00023050 sleeping ...
Thread 0xa00023150 sleeping ...
Thread 0xa000234a0 sleeping ...
Thread 0xa000235a0 sleeping ...
Thread 0xa000236a0 sleeping ...
Thread exited with exception: Exception from 0xa000229f0
Thread exited with exception: Exception from 0xa00022c10
Thread exited with exception: Exception from 0xa00023050
Thread exited with exception: Exception from 0xa000234a0
Thread exited with exception: Exception from 0xa000236a0
просто используйте
std::future
иstd::promise
.