我怎样才能传播线程之间的exception?

我们有一个单线程调用的函数(我们将其命名为主线程)。 在函数体内,我们产生了多个工作线程来完成CPU密集型工作,等待所有线程完成,然后在主线程上返回结果。

结果是调用者可以天真地使用这个函数,并且在内部它将使用多个核心。

迄今为止都很好..

我们面临的问题是处理例外。 我们不希望工作线程上的exception使应用程序崩溃。 我们希望调用者的函数能够在主线程上捕获它们。 我们必须在工作线程上捕获exception,并将它们传播到主线程,让它们从这里继续展开。

我们怎么做到这一点?

我能想到的最好的是:

  1. 在我们的工作线程(std :: exception和我们自己的几个)上捕获各种exception。
  2. loggingexception的types和消息。
  3. 在主线程上有一个相应的switch语句,它重新引发工作线程上logging的任何types的exception。

这具有明显的缺点,即只支持一组有限的exceptiontypes,并且每当添加新的exceptiontypes时都需要修改。

C ++ 11引入了允许在线程之间传输exception的exception_ptrtypes:

#include<iostream> #include<thread> #include<exception> #include<stdexcept> static std::exception_ptr teptr = nullptr; void f() { try { std::this_thread::sleep_for(std::chrono::seconds(1)); throw std::runtime_error("To be passed between threads"); } catch(...) { teptr = std::current_exception(); } } int main(int argc, char **argv) { std::thread mythread(f); mythread.join(); if (teptr) { try{ std::rethrow_exception(teptr); } catch(const std::exception &ex) { std::cerr << "Thread exited with exception: " << ex.what() << "\n"; } } return 0; } 

因为在你的情况下你有多个工作线程,你将需要为每个线程保留一个exception_ptr。

请注意,exception_ptr是一个共享的类ptr指针,所以您将需要至less保留一个指向每个exception的exception_ptr,否则它们将被释放。

微软具体:如果您使用SEHexception(/ EHa),示例代码也将传输SEHexception,如访问冲突,这可能不是你想要的。

目前,唯一可移植的方法是为所有可能希望在线程之间传输的exceptiontypes编写catch子句,将信息存储在catch子句的某处,然后在稍后使用它来重新抛出exception。 这是Boost.Exception采取的方法。

在C ++ 0x中,你将能够用catch(...)捕获exception,然后使用std::current_exception()将其存储在std::exception_ptr一个实例中。 然后,您可以使用std::rethrow_exception()从相同或不同的线程中重新抛出它。

如果您使用的是Microsoft Visual Studio 2005或更高版本,那么just :: thread C ++ 0x线程库支持std::exception_ptr 。 (免责声明:这是我的产品)。

你的问题是,你可能会从多个线程接收多个exception,因为每个exception都可能失败,也许来自不同的原因。

我假设主线程以某种方式等待线程结束检索结果,或者定期检查其他线程的进度,并且访问共享数据是同步的。

解决scheme简单

简单的解决办法是捕获每个线程中的所有exception,将它们logging在一个共享variables(在主线程中)。

一旦所有的线程完成,决定如何处理例外。 这意味着所有其他线程继续处理,这可能不是你想要的。

解决scheme复杂

如果从另一个线程抛出exception,更复杂的解决scheme是让每个线程检查执行的关键点。

如果一个线程抛出一个exception,它在退出线程之前被捕获,exception对象被复制到主线程中的某个容器中(就像在简单的解决scheme中一样),并且一些共享的布尔variables被设置为true。

而当另一个线程testing这个布尔值时,它看到执行将被中止,并以优雅的方式中止。

当所有线程都放弃时,主线程可以根据需要处理exception。

从线程抛出的exception在父线程中不可捕获。 线程有不同的上下文和堆栈,通常父线程不需要停留在那里,等待孩子完成,这样就可以捕获他们的exception。 这个问题根本没有代码:

 try { start thread(); wait_finish( thread ); } catch(...) { // will catch exceptions generated within start and wait, // but not from the thread itself } 

您将需要捕获每个线程内的exception,并解释主线程中线程的退出状态,以重新引发您可能需要的任何exception。

顺便说一句,在一个线程中捕获的缺点,它是具体实现,如果堆栈展开将完成,即自动variables的析构函数甚至可能不会被调用之前终止被调用。 一些编译器这样做,但不是必需的。

你可以序列化工作线程中的exception,传回到主线程,反序列化,并再次抛出? 我期望,为了这个工作,exception将都必须从相同的类(或至less一小部分类与switch语句的东西再次)派生。 另外,我不确定它们是否可以序列化,我只是在大声思考。

如果你使用的是C ++ 11,那么std::future可能就是你正在寻找的东西:它可以自动地捕获到达工作线程顶端的exception,并将它们传递给父线程调用了std::future::get一点。 (在幕后,这正如@AnthonyWilliams的回答中所发生的一样,它已经被执行了。

缺点是没有标准的方法来“停止关心” std::future ; 甚至它的析构函数也会阻塞,直到任务完成。 [编辑,2017:阻塞析构函数的行为只是std::async返回的伪期货的一个错误,你永远不应该使用它。 正常的期货不会阻止他们的破坏者。 但是如果你使用std::future你仍然不能“取消”任务:即使没有人正在听答案,履行诺言的任务也将继续在幕后运行。]这是一个玩具的例子,可能会澄清我的意思:

 #include <atomic> #include <chrono> #include <exception> #include <future> #include <thread> #include <vector> #include <stdio.h> bool is_prime(int n) { if (n == 1010) { puts("is_prime(1010) throws an exception"); throw std::logic_error("1010"); } /* We actually want this loop to run slowly, for demonstration purposes. */ std::this_thread::sleep_for(std::chrono::milliseconds(100)); for (int i=2; i < n; ++i) { if (n % i == 0) return false; } return (n >= 2); } int worker() { static std::atomic<int> hundreds(0); const int start = 100 * hundreds++; const int end = start + 100; int sum = 0; for (int i=start; i < end; ++i) { if (is_prime(i)) { printf("%d is prime\n", i); sum += i; } } return sum; } int spawn_workers(int N) { std::vector<std::future<int>> waitables; for (int i=0; i < N; ++i) { std::future<int> f = std::async(std::launch::async, worker); waitables.emplace_back(std::move(f)); } int sum = 0; for (std::future<int> &f : waitables) { sum += f.get(); /* may throw an exception */ } return sum; /* But watch out! When f.get() throws an exception, we still need * to unwind the stack, which means destructing "waitables" and each * of its elements. The destructor of each std::future will block * as if calling this->wait(). So in fact this may not do what you * really want. */ } int main() { try { int sum = spawn_workers(100); printf("sum is %d\n", sum); } catch (std::exception &e) { /* This line will be printed after all the prime-number output. */ printf("Caught %s\n", e.what()); } } 

我只是试着用std::threadstd::exception_ptr写一个类似于工作的例子,但std::exception_ptr (使用libc ++)出错了,所以我还没有得到它的实际工作。 🙁

[编辑,2017:

 int main() { std::exception_ptr e; std::thread t1([&e](){ try { ::operator new(-1); } catch (...) { e = std::current_exception(); } }); t1.join(); try { std::rethrow_exception(e); } catch (const std::bad_alloc&) { puts("Success!"); } } 

我不知道2013年我做错了什么,但我确定这是我的错。]

事实上,没有一个好的,通用的方法来将exception从一个线程传送到另一个线程。

如果,因为它应该,所有你的exception派生std ::exception,那么你可以有一个顶级的一般exception捕获,将以某种方式发送exception到主线程,它将被抛出。 问题是你失去了例外的抛出点。 你也许可以编写依赖于编译器的代码来获取这些信息并通过它传递。

如果不是所有的exceptioninheritance了std :: exception,那么你就麻烦了,不得不在你的线程中编写大量的顶级catch …但是解决scheme依然成立。

你将需要为worker中的所有exception(包括非stdexception,比如访问冲突)做一个通用的捕获,并且从worker线程(我想你有一些types的消息到位?线程,包含一个指向exception的活动指针,并通过创buildexception副本重新抛出。 然后工人可以释放原始物体并退出。

请参阅http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html 。 也可以写一个你调用的任何函数的包装函数来join一个子线程,该子线程会自动重新引发(使用boost :: rethrow_exception)子线程发出的任何exception。