检测异常未被用户捕获而不重新抛出

Posted

技术标签:

【中文标题】检测异常未被用户捕获而不重新抛出【英文标题】:Detect that exception is not caught by user without rethrowing 【发布时间】:2011-07-14 04:17:13 【问题描述】:

我有一个特定的异常类(用作线程取消异常,我让取消点抛出它)。

如果用户捕捉到它但不重新抛出它,我希望能够让它调用 abort()。为了防止用户试图取消取消或无意中使用了 catch (...)。

除非设置了异常类中的私有标志,否则我尝试在析构函数中调用 abort()。我线程的run函数是这个异常类的朋友,修改了内部标志。问题是在 Visual C++ 中异常被销毁了两次(似乎是因为它在抛出时被复制)。

我想使用引用计数,这样当复制发生时(这似乎是在 throw 语句的执行期间),计数器会递增,并且在有其他副本时析构函数不会抛出异常。

不幸的是,复制构造函数没有实际上被调用了——我试图从中找出来。赋值运算符、移动构造函数和赋值也是如此——它们不会被调用。由于析构函数被抛出两次,因此似乎在没有调用任何这些异常的情况下神奇地复制了异常。

那么我可以在这里使用什么解决方法?

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 编辑~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~

添加示例代码以便解释:

C++0x std::thread 构造函数采用函子或函数和 0..n 个可移动参数。由于 MSVC 不支持可变参数模板,因此我对不同数量的参数使用了重载的构造函数。例如,对于两个参数:

template<typename C, typename A0, typename A1>
inline thread::thread(C const &func, A0 &&arg0, A1 &&arg1)

    Start(make_shared<function<void (void)>>(function<void (void)>(std::bind(forward<C>(func), forward<A0>(arg0), forward<A1>(arg1)))));

接下来,Start() 将 std::function 与侵入式智能自指针打包,以确保 std::function 在 Start() 和线程 Run() 超出范围时被正确销毁_beginthreadex 启动的函数。在 run 函数中,实际运行用户提供的函数/参数的 try/catch 通常是:

try

    (*pack->runpack)();

catch (...)

    terminate();

但是,除了通常的 std::thread 功能之外,我还希望能够执行类似于在 Linux 上调用 pthread_cancel() 时发生的事情,并且能够执行任何一种取消类型类似物PTHREAD_CANCEL_DEFERRED 和 PTHREAD_CANCEL_ASYNCHRONOUS。这一点很重要,因为我正在编写一个信息亭应用程序,在该应用程序中,主应用程序需要能够(大部分时间)从运行第三方创建的模块时挂起的线程中恢复。 我在上面的catch (...) 上面添加了以下内容:

catch (Cancellation const &c) // TODO: Make const & if removing _e flag from Cancellation; TODO: Catch by const & if not modifying internal state

    c.Exec();

其中 Cancellation::Exec() 使用指向当前 std::thread 的指针(我存储在线程本地)并调用 detach()。然后我添加了两个函数,第一个是:

bool CancelThreadSync(std::thread &t, unsigned int ms)

    if (!QueueUserAPC(APCProc, t.native_handle(), 0)) THROW_LASTWINERR(runtime_error, "Could not cancel thread")
    if (ms) Wait(ms);
    if (t.joinable()) return false;
    return true;

APCProc 设置了一些标志,以便我可以添加一个 TestCancel(),类似于 pthread_testcancel() 取消点函数。如果设置了标志,TestCancel() 会抛出 Cancellation(Linux 上的 pthreads 导致堆栈被展开,并且以不同的方式调用析构函数,但它确实这样做了,所以它比 TerminateThread() 好得多)。但是,我还将所有使用等待的地方(例如 WaitForSingleObject 和 Sleep)更改为这些函数的警报版本,以及 WaitForSingleObjectEx 和 SleepEx。例如:

inline void mutex::lock(void)

    while (Load(_owned) || _interlockedbittestandset(reinterpret_cast<long *>(&_waiters), 0))
    
        unsigned long waiters(Load(_waiters) | 1);
        if (AtomicCAS(&_waiters, waiters, waiters + 512) == waiters) // Indicate sleeping
        
            long const ret(NtWaitForKeyedEvent(_handle, reinterpret_cast<void **>(this), 1, nullptr)); // Sleep
            #pragma warning(disable : 4146) // Negating an unsigned
            AtomicAdd(&_waiters, -256); // Indicate finished waking
            #pragma warning(default : 4146)
            if (ret)
            
                if (ret != STATUS_USER_APC) throw std::runtime_error("Failed to wait for keyed event");
                TestCancel();
            
        
    

(来自http://www.locklessinc.com/articles/的原始算法) 请注意检查 APC 是否警告等待。现在我可以取消一个正在运行的线程,即使它死锁了。 TestCancel() 抛出一个 Cancellation 异常,因此堆栈在线程终止之前调用析构函数展开——这正是我想要的。我还为块不在系统调用中的情况添加了异步取消,但可能是无限循环或非常慢的计算或自旋锁死锁。我通过暂停线程、将 Eip(或 x64 上的 Rip)设置为指向抛出 Cancellation 的函数并恢复它来做到这一点。它不是非常强大,但在大多数情况下都可以工作,这对我来说已经足够了。 我已经测试了一些东西,它们工作得很好。我的问题是如何防止用户在不重新抛出的情况下捕获取消。我希望它始终被允许传播到线程运行函数。所以我想我会在 ~Cancellation() 中调用 abort() ,除非设置了内部标志(其中线程运行函数是 Cancellation 的朋友,并且唯一可以做到这一点)。问题是发生了多次调用析构函数,而我无法通过引用计数来解决这个问题,因为据我所知,异常的重复似乎在没有调用任何复制/移动构造函数/赋值的情况下发生(我通过向所有这些添加打印语句进行测试。

【问题讨论】:

你能提供最少的代码来清楚地解释你的问题吗? 提供代码。如果有一个副本,那么它使用复制构造函数。 “如果用户抓住它但不重新抛出它,我希望能够让它调用 abort()。”如果用户想扔别的东西,你会怎么做?你知道,异常翻译吗? Benjamin Lindley,证明你的评论是正确的。您不妨责怪 POSIX 设计人员为 pthread 添加取消功能。 iammilind 和 Martin,我已经用代码编辑了我的代码。我希望这能消除我对问题的糟糕解释所造成的混乱。谢谢。 【参考方案1】:

查看this question 看看是否对您有帮助。正如那里所解释的,当您调用throw 时,您传递一个数据对象(在您的情况下是一个类)来抛出:它被复制并传递给处理该类型的所有 catch 块。 (如果您的 catch 直接使用该类型,则存在另一个副本,如果它使用对该类型的引用,则省略。)

所以你会期望异常对象被删除至少两次:一次是当你用来初始化throw的实例被删除时,一次是当最后一个异常处理程序被调用并且传递给catch 块的对象被删除。

【讨论】:

谢谢。似乎在没有调用任何复制/移动构造函数/赋值的情况下以某种方式复制了异常。我尝试将 printint 语句添加到所有这些语句中,但没有一个被调用,但是当我使用此方法进行检查时,确实至少调用了两次析构函数。 @Borislav - 我不知道异常是如何被复制的:你没有调用你的复制构造函数或赋值运算符听起来很奇怪!但可以肯定的是,异常对象有 2 个实例。【参考方案2】:

abort 是 C 运行时函数,不适用于 C++ 和 Windows SEH 异常。躲开它。它不会被catch__try/__except 捕获。 其次,我建议你启用标志/EHs(C++ -> 代码生成 -> 启用 C++ 异常)。这将有助于您在 try-catch 块中捕获 C++ 和 Windows SEH 异常。它还允许调用任何挂起的析构函数(但嵌套在调用堆栈中)。请记住 _try/_except 不允许调用析构函数。

【讨论】:

抱歉,我的问题不清楚。我想要一个 abort() 来阻止用户捕获这个异常。否则不会被抛出。为了清楚起见,我将编辑我的问题。

以上是关于检测异常未被用户捕获而不重新抛出的主要内容,如果未能解决你的问题,请参考以下文章

捕获和重新抛出 .NET 异常的最佳实践

捕获异常并重新抛出它,但这不是异常

第十二章 重新抛出异常与异常链

java 重新抛出异常

Python Cookbook(第3版)中文版:14.10 重新抛出被捕获的异常

使用条件异常处理调试回原始错误,即重新抛出