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

时间:2011-07-14 04:17:14

标签: c++ multithreading visual-c++ exception

我有一个异常类(用作线程取消异常,我将取消点扔掉)。

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

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

我想使用引用计数,以便在发生复制时(看起来是在执行throw语句期间),计数器会递增,并且析构函数不会在有其他副本时抛出中止。

不幸的是,复制构造函数实际上被调用了 - 我尝试通过它尝试cout。对于赋值运算符,移动构造函数和赋值 - 它们不会被调用。似乎异常是神奇的重复,没有任何这些被调用,因为析构函数被抛出两次。

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

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

添加了示例代码以便解释:

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。这很重要,因为我正在编写一个kiosk应用程序,主应用程序需要能够在运行第三方创建的模块时从挂起的线程中恢复(大部分时间)。
我在上面的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()会抛出取消(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并恢复它的函数来做到这一点。它不是很完美,但在大多数情况下都适用,这对我来说已经足够了
我测试了一些东西,但它们工作得很好。我的问题是如何防止用户捕获取消而不重新抛出它。我希望它始终被允许传播到线程运行函数。所以我想我会在~Anganlation()中调用abort(),除非设置了内部标志(其中线程运行函数是Cancellation的朋友,并且唯一可以执行此操作)。问题是多次调用析构函数,并且我无法通过引用计数来解决这个问题,因为异常的重复似乎在没有调用任何复制/移动构造函数/赋值的情况下发生,我可以告诉它(我通过向所有这些添加打印语句来测试。

2 个答案:

答案 0 :(得分:1)

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

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

答案 1 :(得分:0)

abort是C运行时函数,与C ++和Windows SEH异常不兼容。躲开它。它不会被catch__try / __except捕获。 其次,我建议您启用标记/EHs(C ++ - &gt;代码生成 - &gt;启用C ++异常)。这将有助于您在try-catch块中捕获C ++和Windows SEH异常。它还允许调用任何挂起的析构函数(无论嵌套在callstack中)。记住_ 尝试/ _except不允许调用析构函数。