【发布时间】:2008-11-30 16:55:33
【问题描述】:
C++ 中多线程和异常安全之间的矛盾是什么?有没有好的指导方针可以遵循?线程是否因为未捕获的异常而终止?
【问题讨论】:
标签: c++ multithreading exception c++11
C++ 中多线程和异常安全之间的矛盾是什么?有没有好的指导方针可以遵循?线程是否因为未捕获的异常而终止?
【问题讨论】:
标签: c++ multithreading exception c++11
C++0x 将具有Language Support for Transporting Exceptions between Threads,因此当工作线程抛出异常时,生成线程可以捕获或重新抛出它。
来自提案:
namespace std {
typedef unspecified exception_ptr;
exception_ptr current_exception();
void rethrow_exception( exception_ptr p );
template< class E > exception_ptr copy_exception( E e );
}
【讨论】:
我相信 C++ 标准没有提及多线程 - 多线程是特定于平台的特性。
我不完全确定 C++ 标准对一般未捕获异常的规定,但根据 this page,发生的情况是平台定义的,您应该在编译器的文档中找到。
在我对 g++ 4.0.1(具体来说是 i686-apple-darwin8-g++-4.0.1)进行的快速而肮脏的测试中,结果是调用了 terminate(),这会杀死整个程序.我使用的代码如下:
#include <stdio.h>
#include <pthread.h>
void *threadproc(void *x)
{
throw 0;
return NULL;
}
int main(int argc, char **argv)
{
pthread_t t;
pthread_create(&t, NULL, threadproc, NULL);
void *ret;
pthread_join(t, &ret);
printf("ret = 0x%08x\n", ret);
return 0;
}
使用g++ threadtest.cc -lpthread -o threadtest 编译。输出是:
terminate called after throwing an instance of 'int'
【讨论】:
未捕获的异常将调用terminate(),后者又调用terminate_handler(可由程序设置)。默认情况下,terminate_handler 调用 abort()。
即使您覆盖默认的terminate_handler,该标准仍规定您提供的例程“应终止程序的执行而不返回调用者”(ISO 14882-2003 18.6.1.3)。
因此,总而言之,未捕获的异常将终止程序,而不仅仅是线程。
就线程安全而言,正如Adam Rosenfield 所说,这是标准未解决的特定于平台的问题。
【讨论】:
这是 Erlang 存在的最大原因。
我不知道约定是什么,但恕我直言,尽可能像 Erlang。使堆对象不可变并设置某种消息传递协议以在线程之间进行通信。避免锁。确保消息传递是异常安全的。在堆栈上保留尽可能多的有状态内容。
【讨论】:
正如其他人所讨论的,并发(尤其是线程安全)是一个架构问题,它会影响您设计系统和应用程序的方式。
但我想回答您关于异常安全和线程安全之间的紧张关系的问题。
在类级别线程安全需要更改接口。就像异常安全一样。例如,习惯上类会返回对内部变量的引用,比如:
class Foo {
public:
void set_value(std::string const & s);
std::string const & value() const;
};
如果 Foo 被多个线程共享,麻烦在等着你。自然地,您可以放置一个互斥锁或其他锁来访问 Foo。但很快,所有 C++ 程序员都希望将 Foo 包装到“ThreadSafeFoo”中。我的论点是 Foo 的接口应该改为:
class Foo {
public:
void set_value(std::string const & s);
std::string value() const;
};
是的,它更昂贵,但可以通过 Foo 内部的锁使其成为线程安全的。 IMnsHO 这在线程安全和异常安全之间产生了一定的张力。或者至少,您需要执行更多分析,因为每个用作共享资源的类都需要在两种情况下进行检查。
【讨论】:
一个经典的例子(不记得我第一次看到的地方)在 std 库中。
以下是从队列中弹出内容的方法:
T t;
t = q.front(); // may throw
q.pop();
与以下相比,此界面有些迟钝:
T t = q.pop();
但是完成是因为 T 拷贝赋值可以抛出。如果在弹出发生后复制抛出,则该元素将从队列中丢失,并且永远无法恢复。但由于复制发生在元素弹出之前,您可以在 try/catch 块中对来自 front() 的复制进行任意处理。
缺点是你不能用 std::queue 的接口实现一个线程安全的队列,因为涉及到两个步骤。对异常安全(分离可能抛出的步骤)有利的东西现在对多线程不利。
您在异常安全方面的主要救星是指针操作是不抛出的。同样,指针操作可以在大多数平台上进行原子操作,因此它们通常可以成为您在多线程代码中的救星。你可以吃蛋糕也可以吃,但是真的很难。
【讨论】:
我注意到两个问题:
在 Linux 上的 g++ 中,线程 (pthread_cancel) 的终止是通过抛出“未知”异常来完成的。一方面,这可以让你在线程被杀死时很好地清理。另一方面,如果您捕获该异常并且不重新抛出它,您的代码将以 abort() 结束。因此,如果您或您使用的任何库杀死线程,则不能拥有
捕捉(...)
没有
throw;
在您的线程代码中。 Here 是对网络上这种行为的引用:
【讨论】:
我不建议让任何异常保持未捕获。将您的顶级线程函数包装在可以更优雅(或至少详细地)关闭程序的包罗万象的处理程序中。
【讨论】:
我认为最重要的是要记住,来自其他线程的未捕获异常不会向用户显示或在主线程中抛出。因此,您必须使用 try/catch 块扭曲所有应该在不同于主线程的线程上运行的代码。
【讨论】: