【问题标题】:Writing Multithreaded Exception-Safe Code编写多线程异常安全代码
【发布时间】:2008-11-30 16:55:33
【问题描述】:

C++ 中多线程和异常安全之间的矛盾是什么?有没有好的指导方针可以遵循?线程是否因为未捕获的异常而终止?

【问题讨论】:

    标签: c++ multithreading exception c++11


    【解决方案1】:

    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 );
    }
    

    【讨论】:

    【解决方案2】:

    我相信 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() 将导致所有线程被杀死而不发生进一步的处理。
    • 附言。这是我使用过的所有 pthread 库的行为。
    【解决方案3】:

    未捕获的异常将调用terminate(),后者又调用terminate_handler(可由程序设置)。默认情况下,terminate_handler 调用 abort()

    即使您覆盖默认的terminate_handler,该标准仍规定您提供的例程“应终止程序的执行而不返回调用者”(ISO 14882-2003 18.6.1.3)。

    因此,总而言之,未捕获的异常将终止程序,而不仅仅是线程。

    就线程安全而言,正如Adam Rosenfield 所说,这是标准未解决的特定于平台的问题。

    【讨论】:

      【解决方案4】:

      这是 Erlang 存在的最大原因。

      我不知道约定是什么,但恕我直言,尽可能像 Erlang。使堆对象不可变并设置某种消息传递协议以在线程之间进行通信。避免锁。确保消息传递是异常安全的。在堆栈上保留尽可能多的有状态内容。

      【讨论】:

        【解决方案5】:

        正如其他人所讨论的,并发(尤其是线程安全)是一个架构问题,它会影响您设计系统和应用程序的方式。

        但我想回答您关于异常安全和线程安全之间的紧张关系的问题。

        在类级别线程安全需要更改接口。就像异常安全一样。例如,习惯上类会返回对内部变量的引用,比如:

        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 这在线程安全和异常安全之间产生了一定的张力。或者至少,您需要执行更多分析,因为每个用作共享资源的类都需要在两种情况下进行检查。

        【讨论】:

          【解决方案6】:

          一个经典的例子(不记得我第一次看到的地方)在 std 库中。

          以下是从队列中弹出内容的方法:

          T t;
          t = q.front(); // may throw
          q.pop();
          

          与以下相比,此界面有些迟钝:

          T t = q.pop();
          

          但是完成是因为 T 拷贝赋值可以抛出。如果在弹出发生后复制抛出,则该元素将从队列中丢失,并且永远无法恢复。但由于复制发生在元素弹出之前,您可以在 try/catch 块中对来自 front() 的复制进行任意处理。

          缺点是你不能用 std::queue 的接口实现一个线程安全的队列,因为涉及到两个步骤。对异常安全(分离可能抛出的步骤)有利的东西现在对多线程不利。

          您在异常安全方面的主要救星是指针操作是不抛出的。同样,指针操作可以在大多数平台上进行原子操作,因此它们通常可以成为您在多线程代码中的救星。你可以吃蛋糕也可以吃,但是真的很难。

          【讨论】:

          • 我不明白您是如何得出结论的“由于涉及两个步骤,您无法使用 std::queue 的接口实现线程安全的队列”。
          • 想一想——即使你锁定了对内部数据结构的访问,因为在调用 front 和 pop 之间没有持有该锁,在你获得 front 元素后,其他一些线程也可以在弹出之前获取相同的前面元素,然后您会丢失一个元素并且重复一个元素。
          【解决方案7】:

          我注意到两个问题:

          • 在 Linux 上的 g++ 中,线程 (pthread_cancel) 的终止是通过抛出“未知”异常来完成的。一方面,这可以让你在线程被杀死时很好地清理。另一方面,如果您捕获该异常并且不重新抛出它,您的代码将以 abort() 结束。因此,如果您或您使用的任何库杀死线程,则不能拥有

            捕捉(...)

          没有

          throw;
          

          在您的线程代码中。 Here 是对网络上这种行为的引用:

          • 有时您需要在线程之间传输异常。这不是一件容易的事 - 我们最终做了一些hack,当正确的解决方案是您在进程之间使用的那种编组/解组时。

          【讨论】:

            【解决方案8】:

            我不建议让任何异常保持未捕获。将您的顶级线程函数包装在可以更优雅(或至少详细地)关闭程序的包罗万象的处理程序中。

            【讨论】:

            • 虽然这 可以 对某些应用程序有意义,但我不同意将其作为一般建议。通常我建议人们在有意义的时候处理异常,否则让它们传播。如果您正确使用了 RAII,则关闭应该不是问题。
            • 除非你的 RAII 析构函数在没有被捕获的情况下不会被调用。未捕获的异常是否展开堆栈取决于实现。它只能中止。如果您在顶部捕获所有内容,则可以保证堆栈展开。
            • @onebyone:根据规范中的哪一段?就像所有 C++ 大师(Herb Sutter 等)都建议编写不使用 try/catch 而是“与异常无关”的代码。
            • 我指的不是析构函数问题。如果你不处理异常,整个程序都会被杀死(对吧?)。根据正在完成的任务,您可以为线程池生成一个新线程,在退出之前完成一些 I/O,打印额外的调试信息等。
            • 通常这是你在 C++ 中想要的。 C++ 异常!= Java 异常。如果您查看 C++ 中定义的异常类型,它们是 bad_alloc、logic_error、runtime_error 和 out_of_bounds 类型。不是 EndOfFileException 之类的。在 C++ 中,异常用于错误而不是流控制。
            【解决方案9】:

            我认为最重要的是要记住,来自其他线程的未捕获异常不会向用户显示或在主线程中抛出。因此,您必须使用 try/catch 块扭曲所有应该在不同于主线程的线程上运行的代码。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2014-10-08
              • 1970-01-01
              • 2011-03-08
              • 1970-01-01
              • 2011-04-06
              • 2011-07-07
              • 2013-02-20
              • 1970-01-01
              相关资源
              最近更新 更多