C++ 中多线程和异常安全之间的矛盾是什么?有没有好的指导方针可以遵循?线程是否因为未捕获的异常而终止?
9 回答
C++0x 将为在线程之间传输异常提供语言支持,以便当工作线程抛出异常时,生成线程可以捕获或重新抛出它。
从提案中:
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++ 标准对一般未捕获异常的规定,但根据此页面,发生的情况是平台定义的,您应该在编译器的文档中找到。
在我对 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()
.
即使您覆盖了 default 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;
在您的线程代码中。以下是网络上此行为的参考:
- 有时您需要在线程之间传输异常。这不是一件容易的事——我们最终做了一些hack,当正确的解决方案是你在进程之间使用的那种编组/解组时。
我不建议让任何异常保持未捕获。将您的顶级线程函数包装在可以更优雅(或至少详细地)关闭程序的包罗万象的处理程序中。
我认为最重要的是要记住来自其他线程的未捕获异常不会向用户显示或在主线程中抛出。因此,您必须使用 try/catch 块扭曲所有应该在与主线程不同的线程上运行的代码。