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


105

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

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

到目前为止一切都很好。

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

我们应该怎么做?

我能想到的最好的是:

  1. 在我们的工作线程上捕获各种各样的异常(std :: exception和我们自己的一些异常)。
  2. 记录异常的类型和消息。
  3. 在主线程上有一个对应的switch语句,该语句会引发工作线程上记录的任何类型的异常。

这具有明显的缺点,即仅支持有限的一组异常类型,并且每当添加新的异常类型时都需要进行修改。

Answers:


89

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 Exceptions(/EHa),示例代码还将传输SEH异常,例如访问冲突,这可能不是您想要的。


主线程产生多线程呢?如果第一个线程遇到异常并退出,则main()将在可能永远运行的第二个线程join()中等待。在两次join()之后,main()将永远无法测试teptr。似乎所有线程都需要定期检查全局teptr并在适当的情况下退出。有没有一种干净的方法来处理这种情况?
Cosmo

75

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

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

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


7
现在,它已成为C ++ 11的一部分,并受到MSVS 2010的支持。请参阅msdn.microsoft.com/en-us/library/dd293602.aspx
2012年

7
Linux上的gcc 4.4+也支持它。
安东尼·威廉姆斯


11

如果您使用C ++ 11,那么std::future可能你正在寻找什么:它可以自动地捕获异常,它作出的工作线程的顶部,并通过父线程的一点通过他们std::future::get是叫。(在幕后,这与@AnthonyWilliams的回答完全相同;它已经为您实现了。)

不利的一面是,没有“停止关心” a的标准方法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::thread和编写一个类似工作的示例std::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年做错了什么,但我敢肯定这是我的错。]


为什么您将创建未来分配给命名对象f,然后将emplace_back其分配?您不能只是做一下,waitables.push_back(std::async(…));还是我忽略了某些内容(它会编译,问题是它是否可能泄漏,但我不知道如何)?
康拉德·鲁道夫

1
另外,是否有一种方法可以通过中止期货而不是期货来平仓wait?顺带一提,“一旦一项工作失败,其他工作就不再重要了”。
康拉德·鲁道夫

4年后,我的回答还不成熟。:)关于“为什么”:我认为这只是为了清楚(表明async返回未来而不是其他)。关于“还存在”的问题:不在中std::future,但是如果您不介意为初学者重写整个STL ,请参见Sean Parent的演讲“更好的代码:并发性”或我的“ Scratch的未来”,以不同的方式实现这一点。:)关键搜索词是“取消”。
Quuxplusone

感谢您的回复。一分钟后,我一定会看一下谈判。
康拉德·鲁道夫

1
好2017年。与接受的相同,但具有范围异常指针。我会将其放在顶部,甚至可以摆脱其余部分。
内森·库珀

6

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

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

简单的解决方案

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

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

复杂的解决方案

更为复杂的解决方案是,如果从另一个线程引发了异常,则让您的每个线程都在执行的关键时刻进行检查。

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

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

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


4

从线程引发的异常将无法在父线程中捕获。线程具有不同的上下文和堆栈,通常不需要父线程停留在那里并等待子进程完成,以便它可以捕获其异常。在代码中根本没有该捕获的地方:

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

您将需要在每个线程内捕获异常,并解释主线程中线程的退出状态,以重新引发您可能需要的所有异常。

顺便说一句,在线程中没有catch的情况下,是否完全执行堆栈展开是特定于实现的,即,在调用终止之前甚至可能不会调用自动变量的析构函数。一些编译器可以这样做,但这不是必需的。


3

您能否在工作线程中序列化异常,将该异常发送回主线程,反序列化,然后再次抛出?我希望,要使此方法正常工作,所有异常都必须源自同一类(或者至少再次使用switch语句生成一小类类)。另外,我不确定它们是否可序列化,我只是大声考虑。


如果两个线程都在同一进程中,为什么需要序列化它?
纳瓦兹

1
@Nawaz,因为异常可能引用了其他线程不会自动使用的线程局部变量。
tvanfosson

2

实际上,没有很好的通用方法将异常从一个线程传输到另一个线程。

如果所有异常都应从std :: exception派生,那么您可以拥有一个顶级通用异常捕获,它将以某种方式将异常发送到主线程,并在该主线程中再次引发该异常。问题是您失去了异常的抛出点。您可能可以编写依赖于编译器的代码来获取此信息并通过它进行传输。

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


1

您将需要对工作程序中的所有异常(包括非标准异常,如访问冲突)进行通用捕获,并从工作程序线程中发送一条消息(我想您已经有某种消息传递方式了吗)到控制端线程,其中包含指向异常的活动指针,然后通过创建异常的副本将其重新抛出。然后,工作人员可以释放原始对象并退出。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.