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

How can I propagate exceptions between threads?

提问人:pauldoo 提问时间:10/24/2008 更新时间:9/19/2018 访问量:56992

问:

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

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

到目前为止一切都很好。

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

我们怎样才能做到这一点?

我能想到的最好的是:

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

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

C++ 多线程 异常

评论


答:

2赞 PierreBdR 10/24/2008 #1

事实上,没有好的通用方法可以将异常从一个线程传输到下一个线程。

如果所有异常都源自 std::exception,那么您可以有一个顶级的通用异常捕获,它会以某种方式将异常发送到主线程,在那里它将再次被抛出。问题是你失去了异常的抛出点。不过,您可以编写依赖于编译器的代码来获取此信息并传输它。

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

3赞 tvanfosson 10/24/2008 #2

您能否在工作线程中序列化异常,将其传输回主线程,反序列化并再次抛出它?我希望要做到这一点,异常都必须来自同一个类(或者至少是一小组带有switch语句的类)。另外,我不确定它们是否可以序列化,我只是在大声思考。

评论

0赞 Nawaz 12/13/2012
如果两个线程都在同一进程中,为什么需要序列化它?
1赞 tvanfosson 12/14/2012
@Nawaz,因为异常可能引用了线程局部变量,而这些变量不会自动提供给其他线程。
1赞 anon6439 10/24/2008 #3

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

6赞 paercebal 10/24/2008 #4

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

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

简单的解决方案

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

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

复杂的解决方案

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

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

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

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

5赞 n-alexander 10/24/2008 #5

从线程引发的异常将无法在父线程中捕获。线程具有不同的上下文和堆栈,通常不需要父线程留在那里并等待子线程完成,以便它可以捕获它们的异常。代码中根本没有这个陷阱的位置:

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

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

顺便说一句,如果线程中没有捕获,则完全可以执行堆栈展开,即在调用 terminate 之前甚至可能不会调用自动变量的析构函数。有些编译器会这样做,但这不是必需的。

77赞 Anthony Williams 10/24/2008 #6

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

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

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

评论

7赞 Johan Råde 1/9/2012
这现在是 C++11 的一部分,并受 MSVS 2010 支持;请参见 msdn.microsoft.com/en-us/library/dd293602.aspx
7赞 Anthony Williams 1/9/2012
Linux 上的 gcc 4.4+ 也支持它。
0赞 Alexis Wilke 11/18/2013
很酷,有一个使用示例的链接:en.cppreference.com/w/cpp/error/exception_ptr
1赞 Emil 1/14/2010 #7

请参见 http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html。也可以编写一个包装函数,其中包含您调用的任何函数来连接子线程,该函数会自动重新抛出(使用 boost::rethrow_exception)子线程发出的任何异常。

15赞 Quuxplusone 1/25/2013 #8

如果你使用的是 C++11,那么它可能会做你正在寻找的事情:它可以自动捕获进入工作线程顶部的异常,并将它们传递到调用点的父线程。(在幕后,这与@AnthonyWilliams的回答完全一样;它刚刚为你实现。std::futurestd::future::get

不利的一面是没有标准的方法来“停止关心”一个;甚至它的析构函数也会简单地阻塞,直到任务完成。[编辑,2017 年:阻塞析构函数行为只是std::async 返回的伪期货的错误特征,无论如何您都不应该使用它。正常期货不会阻塞其析构函数。但是,如果您使用 std::future,您仍然无法“取消”任务:即使没有人再听答案,履行承诺的任务也会继续在幕后运行。这里有一个玩具的例子,可以解释我的意思: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());
    }
}

我只是尝试使用 和 编写一个类似工作的例子,但是(使用 libc++)出了点问题,所以我还没有让它真正工作。:(std::threadstd::exception_ptrstd::exception_ptr

[编辑,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年做错了什么,但我确信这是我的错。

评论

1赞 Konrad Rudolph 7/17/2017
为什么要将 creates future 分配给一个命名的,然后再分配它?你不能做还是我忽略了一些东西(它编译了,问题是它是否会泄漏,但我不明白如何)?femplace_backwaitables.push_back(std::async(…));
1赞 Konrad Rudolph 7/17/2017
另外,有没有办法通过中止期货而不是 ing 来平仓堆栈?类似“一旦其中一项工作失败,其他工作就不再重要”之类的东西。wait
0赞 Quuxplusone 7/18/2017
4年后,我的答案并没有过时。:)回复“为什么”:我认为这只是为了清楚起见(表明返回的是未来而不是其他东西)。Re “Also, is there”:不在 中,但如果您不介意为初学者重写整个 STL,请参阅 Sean Parent 的演讲“Better Code: Concurrency”或我的“Futures from Scratch”,了解实现它的不同方法。:)关键的搜索词是“取消”。asyncstd::future
0赞 Konrad Rudolph 7/18/2017
感谢您的回复。当我有时间的时候,我一定会看看这些演讲。
2赞 Nathan Cooper 2/27/2019
好的 2017 编辑。与接受的指针相同,但具有作用域内的异常指针。我会把它放在顶部,甚至可能摆脱其余的。
114赞 Gerardo Hernandez 9/7/2015 #9

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

请注意,这是一个共享的类似 ptr 的指针,因此您需要至少保留一个指向每个异常的指针,否则它们将被释放。exception_ptrexception_ptr

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

评论

0赞 Cosmo 7/18/2019
从主线程生成的多个线程呢?如果第一个线程遇到异常并退出,main() 将在第二个线程 join() 处等待,该线程可能会永远运行。main() 在两个 joins() 之后永远不会测试 teptr。似乎所有线程都需要定期检查全局 teptr 并在适当时退出。有没有一种干净的方法来处理这种情况?