Как распространять исключения из подфункций в многопоточной программе?

Я пытаюсь понять, как исключения могут распространяться между различными функциями и возвращаться в основную функцию на С++. У меня есть небольшая установка:

основной.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: Приведенный выше код является всего лишь примером. Если какая-либо информация отсутствует, пожалуйста, дайте мне знать.

просто используйте std::future и std::promise.

Marek R 19.03.2024 17:50

Вы устанавливаете g_exceptionPtr в треде, а затем перезаписываете его в основной ветке.

interjay 19.03.2024 17:55

Примечание: std::cerr имеет модульную буферизацию, т. е. каждый стандартный инструмент вставки потока сбрасывает поток после вывода. Нет необходимости в дополнительном сливе, который делает std::endl. Используйте '\n', чтобы завершить строку, если нет веской причины не делать этого.

Pete Becker 19.03.2024 17:56

@interjay, да, это не нужно. Но если я удалю его, у меня будет такое же поведение

Preeti 19.03.2024 17:56
std::rethrow_exception(std::current_exception()); это просто throw;...
Jarod42 19.03.2024 18:03

Привет @MarekR, не могли бы вы рассказать мне, как мне использовать std::future и std::promise здесь?

Preeti 19.03.2024 18:05

@Jarod42 Jarod42, я пробовал использовать throw, но поведение такое же. Он не распространяется на main.cpp

Preeti 19.03.2024 18:07

Я не говорил, что это нужно делать, просто эквивалентность (чтобы также лучше понять, почему это неправильный путь). (в непрерывности try { foo(); } catch(...) { throw;} — это просто foo();.

Jarod42 19.03.2024 18:18

@Preeti то, что вы описываете, невозможно для нормального поведения try/catch/throw, исключение не перехватывается где-либо еще до того, как оно достигнет main, поэтому здесь явно задействован еще один аспект, который вы плохо объясняете. Пожалуйста, отредактируйте свой вопрос, чтобы предоставить лучший минимально воспроизводимый пример, который действительно демонстрирует реальную проблему в действии.

Remy Lebeau 19.03.2024 18:23

Если вы запросите трассировку стека, см. en.cppreference.com/w/cpp/utility/basic_stacktrace

Sam Ginrich 19.03.2024 22:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
11
108
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы вызываете 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 19.03.2024 18:03

@Preeti Дело в том, что все, что выбрасывает testThread, вам нужно поймать и установить exceptionPtr так, чтобы оно указывало на current_exception, что затем можно повторно выбросить после join()запуска потока.

Ted Lyngmo 19.03.2024 18:10

@Привети, судя по тому, как вы изложили проблему, вообще нет смысла использовать std::current_exception() и std::rethrow_exception(). Я обновил свой ответ.

Remy Lebeau 19.03.2024 18:25

Привет @RemyLebeau, спасибо за ответ. Я хотел добавить одну вещь: testThread на самом деле ожидает событий нажатия клавиш и, следовательно, вызывает testException как часть другой логики. Но я также получаю Debug error: abort () has been called от вашего первого решения

Preeti 19.03.2024 18:26

Вы могли бы упомянуть std::promise и std::future, у которых есть для этого оборудование.

Yakk - Adam Nevraumont 19.03.2024 18:52

@Yakk-AdamNevraumont Я мог бы, если бы знал что-нибудь о них. Но я этого не делаю, поэтому я не сделал

Remy Lebeau 19.03.2024 20:27

@Preeti, это означает, что вы генерируете исключение, которое не перехватывается.

Remy Lebeau 19.03.2024 20:29

Привет @RemyLebeau, я также пытался добавить throw std::runtime_error("Testing exception."); в testException внутри try блока, но это не выбрасывается повторно. Внутри try мне нужно вызвать std::thread, так как этот поток будет искать любые события нажатия клавиш, и это повлияет на то, как функция writeToCSV вызывается в разных местах... Но когда я добавляю throw std::runtime_error("Testing exception."); снаружи try (перед вызовом std::thread, это исключение перехватывается и повторно выбрасывается в основной. Я думаю, что поток принудительно завершен, и это обрабатывается неправильно...

Preeti 20.03.2024 08:12

Один из способов решения этой проблемы — использовать 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

Другие вопросы по теме