如何在线程之间传播异常?

时间:2008-10-24 11:17:09

标签: c++ multithreading exception

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

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

到目前为止一切都很好..

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

我们怎么做?

我能想到的最好的是:

  1. 在我们的工作线程上捕获各种异常(std :: exception和我们自己的一些)。
  2. 记录异常的类型和消息。
  3. 在主线程上有一个相应的switch语句,它重新抛出工作线程上记录的任何类型的异常。
  4. 这有一个明显的缺点,即只支持一组有限的异常类型,并且每当添加新的异常类型时都需要修改。

9 个答案:

答案 0 :(得分:72)

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

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

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

答案 1 :(得分:68)

C ++ 11引入了exception_ptr类型,允许在线程之间传输异常:

#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的共享指针,因此您需要至少保留一个exception_ptr指向每个异常,否则它们将被释放。

Microsoft特定:如果您使用SEH例外(/EHa),示例代码也会传输SEH例外,例如访问冲突,这可能不是您想要的。

答案 2 :(得分:9)

如果您正在使用C ++ 11,那么std::future可能正是您正在寻找的内容:它可以自动捕获使其成为工作线程顶部的异常,并将它们传递给调用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年我做错了什么,但我确定这是我的错。]

答案 3 :(得分:6)

问题在于,您可能会从多个线程收到多个异常,因为每个异常都可能失败,可能是出于不同的原因。

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

简单解决方案

简单的解决方案是捕获每个线程中的所有异常,将它们记录在共享变量中(在主线程中)。

完成所有线程后,决定如何处理异常。这意味着所有其他线程继续进行处理,这可能不是您想要的。

复杂解决方案

如果从另一个线程抛出异常,那么更复杂的解决方案是让每个线程在执行的关键点检查它们。

如果一个线程抛出异常,它会在退出线程之前被捕获,异常对象被复制到主线程中的某个容器中(如在简单的解决方案中),并且一些共享的布尔变量被设置为true。

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

当所有线程都中止时,主线程可以根据需要处理异常。

答案 4 :(得分:4)

从线程抛出的异常将无法在父线程中捕获。线程具有不同的上下文和堆栈,并且通常父线程不需要留在那里等待子项完成,以便它可以捕获它们的异常。代码中没有任何地方可以捕获:

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

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

BTW,在线程中没有捕获的情况下,如果将完成堆栈展开,则它是特定于实现的,即在调用终止之前甚至可能不会调用自动变量的析构函数。有些编译器会这样做,但不是必需的。

答案 5 :(得分:3)

您是否可以序列化工作线程中的异常,将其传回主线程,反序列化并再次抛出?我希望为了使这个工作,所有异常都必须从同一个类派生(或者至少有一小部分类再次使用switch语句)。另外,我不确定它们是否可以序列化,我只是在大声思考。

答案 6 :(得分:2)

确实没有好的和通用的方法将异常从一个线程传输到下一个线程。

如果应该这样,所有异常都来自std :: exception,那么你可以有一个顶级的常规异常catch,它会以某种方式将异常发送到主线程,然后再将它抛出。问题是你失去了异常的抛出点。您可以编写与编译器相关的代码来获取此信息并通过它进行传输。

如果不是所有的异常都继承了std :: exception,那么你就麻烦了并且必须在你的线程中编写很多顶级的catch ...但是解决方案仍然存在。

答案 7 :(得分:1)

您需要对worker中的所有异常执行泛型捕获(包括非std异常,例如访问冲突),并从工作线程发送消息(我假设您有某种消息传递?)到控制线程,包含指向异常的实时指针,并通过创建异常副本重新抛出该异常。 然后工人可以释放原始对象并退出。

答案 8 :(得分:1)

http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html。也可以编写一个函数来包含你调用的函数来加入子线程,它会自动重新抛出(使用boost :: rethrow_exception)子线程发出的任何异常。