在线程之间传输异常

Microsoft C++ 编译器 (MSVC) 支持从一个线程向另一个线程传输异常。 通过传输异常,你可以在一个线程中捕获异常,然后使该异常看似是在另一个线程中引发的。 例如,你可以使用该功能编写多线程应用程序,其中主线程将处理其辅助线程引发的所有异常。 传输异常对创建并行编程库或系统的开发人员最有用处。 为实现传输异常,MSVC 提供了 exception_ptr 类型以及 current_exceptionrethrow_exceptionmake_exception_ptr 函数。

语法

namespace std
{
   typedef unspecified exception_ptr;
   exception_ptr current_exception();
   void rethrow_exception(exception_ptr p);
   template<class E>
       exception_ptr make_exception_ptr(E e) noexcept;
}

参数

unspecified
用于实现 exception_ptr 类型的未指定的内部类。

p
引用异常的 exception_ptr 对象。

E
表示异常的类。

e
参数 E 类的实例。

返回值

current_exception 函数返回引用当前进行中的异常的 exception_ptr 对象。 如果没有进行中的异常,该函数将返回未与任何异常关联的exception_ptr对象。

make_exception_ptr函数返回引用e参数指定的异常的exception_ptr对象。

注解

方案

假设你要创建能伸缩以处理可变工作负荷的应用程序。 为了实现此目标,你要设计一个多线程应用程序,其中初始的主线程会创建所需数量的辅助线程,以完成该工作。 辅助线程可帮助主线程管理资源、平衡负载和提高吞吐量。 通过分发工作,多线程应用程序的表现优于单线程应用程序。

但是,如果辅助线程引发异常,你希望主线程予以处理。 这是因为你希望你的应用程序无论有多少辅助线程,都能以一致统一的方式处理异常。

解决方案

为处理先前的方案,C++ 标准支持在线程之间传输异常。 如果辅助线程引发异常,该异常会成为当前异常。 拿现实世界打比方,当前异常就好比处于动态的状态。 当前异常从引发之时到捕获它的异常处理程序返回之时就处于飞行状态。

辅助线程可在 catch 块中捕获当前异常,然后调用 current_exception 函数,将该异常存储在 exception_ptr 对象中。 exception_ptr 对象必须可用于辅助线程和主线程。 例如,exception_ptr 对象可以是全局变量,由 mutex 控制对它的访问。 术语“传输异常”指的是一个线程中的异常可以转换为可由其他线程访问的形式。

接下来,主线程调用 rethrow_exception 函数,该函数提取并继而引发 exception_ptr 对象中的异常。 异常引发后,将成为主线程中的当前异常。 也就是说,该异常看起来源自主线程。

最后,主线程可以在 catch 块中捕获当前异常,然后进行处理或将其抛给更高级别的异常处理程序。 或者,主线程可以忽略该异常并允许该进程结束。

大多数应用程序不必在线程之间传输异常。 但是,该功能在并行计算系统中有用,是因为该系统可以在辅助线程、处理器或内核间分配工作。 在并行计算环境中,单个专用线程可以处理辅助线程中的所有异常,并可以为任何应用程序提供一致的异常处理模型。

有关 C++ 标准委员会建议的详细信息,请在 Internet 中搜索编号为 N2179,标题为“Language Support for Transporting Exceptions between Threads”(在线程之间传输异常的语言支持)的文档。

异常处理模型和编译器选项

你的应用程序的异常处理模型决定了它是否可以捕获和传输异常。 Visual C++ 支持三种处理 C++ 异常的模型:ISO 标准 C++ 异常处理结构化异常处理 (SEH),以及公共语言运行时 (CLR) 异常。 使用/EH/clr编译器选项以指定应用程序的异常处理模型。

只有编译器选项和编程语句的以下组合可以传输异常。 其他组合要么不能捕获异常,要么能捕获但不能传输异常。

  • /EHa编译器选项与catch语句可以传输 SEH 和 C++ 异常。

  • /EHa/EHs/EHsc编译器选项与catch语句可以传输 C++ 异常。

  • /clr编译器选项与catch语句可以传输 C++ 异常。 /clr编译器选项表示/EHa选项的规范。 编译器不支持传输托管异常。 这是因为,从System.Exception 类派生的托管异常已经是可使用公共语言运行时工具在线程间移动的对象。

    重要

    建议指定/EHsc编译器选项并仅捕获 C++ 异常。 如果使用/EHa/clr编译器选项和含有省略号异常声明 (catch(...))的catch语句,就可能面临安全威胁。 你可能希望使用 catch 语句捕获几个特定的异常。 但是,catch(...) 语句将捕获所有的 C++ 和 SEH 异常,包括致命的意外异常。 如果忽略意外异常或处理不当,恶意代码就可以趁此机会破坏你程序的安全性。

使用情况

后面几节将介绍如何使用 exception_ptr 类型以及 current_exceptionrethrow_exceptionmake_exception_ptr 函数传输异常。

exception_ptr 类型

使用 exception_ptr 对象可引用当前异常或用户指定异常的实例。 在 Microsoft 实现中,异常由EXCEPTION_RECORD结构表示。 每个 exception_ptr 对象包含一个异常引用字段,该字段指向表示异常的 EXCEPTION_RECORD 结构的副本。

声明exception_ptr变量时,该变量不与任何异常相关联。 也就是说,其异常引用字段为 NULL。 此类 exception_ptr 对象称为 null exception_ptr

使用 current_exceptionmake_exception_ptr 函数可将异常指派给 exception_ptr 对象。 将异常指派给 exception_ptr 变量时,该变量的异常引用字段将指向该异常的副本。 如果没有足够的内存来复制异常,异常引用字段将指向 std::bad_alloc 异常的副本。 如果current_exceptionmake_exception_ptr函数因任何其他原因不能复制异常,该函数会调用terminate函数以退出当前进程。

虽然名称像是指针,但exception_ptr对象本身不属于指针。 它不遵循指针语义,不能与指针成员访问 (->) 或间接寻址 (*) 运算符一起使用。 exception_ptr 对象没有公共数据成员或成员函数。

比较

你可以使用相等 (==) 和不相等 (!=) 运算符比较两个 exception_ptr 对象。 这两个运算符不比较表示异常的EXCEPTION_RECORD结构的二进制值(位模式)。 而是比较 exception_ptr 对象的异常引用字段中的地址。 因此,null exception_ptr和 NULL 值的比较结果为相等。

current_exception 函数

调用 catch 块中的 current_exception 函数。 如果异常处于动态的状态,而且 catch 块可捕获该异常,current_exception 函数将返回引用该异常的 exception_ptr 对象。 否则,该函数将返回 null exception_ptr 对象。

详细信息

current_exception 函数会捕获动态异常,而不管 catch 语句是否指定 exception-declaration 语句。

如果不重新引发当前异常,会在catch块的末尾调用该异常的析构函数。 但是,即使调用析构函数中的 current_exception 函数,该函数仍返回引用当前异常的 exception_ptr 对象。

current_exception 函数的相继调用将返回引用当前异常的不同副本的 exception_ptr 对象。 因此,由于对象引用不同的副本,即使副本具有相同的二进制值,其比较结果也是不相等。

SEH 异常

如果使用/EHa编译器选项,可以在 C++ catch块中捕获 SEH 异常。 current_exception 函数返回引用 SEH 异常的 exception_ptr 对象。 如果调用rethrow_exception函数,将传输的exception_ptr对象作为其参数,该函数会引发 SEH 异常。

如果你在 SEH __finally 终止处理程序、__except 异常处理程序或 __except 筛选器表达式中调用 current_exception 函数,该函数将返回 null exception_ptr

传输的异常不支持嵌套异常。 如果处理异常时引发了另一个异常,会发生嵌套异常。 如果你捕获嵌套异常,EXCEPTION_RECORD.ExceptionRecord 数据成员将指向描述关联异常的 EXCEPTION_RECORD 结构链。 current_exception函数不支持嵌套异常,因为它返回exception_ptr数据成员调零的ExceptionRecord对象。

如果你捕获 SEH 异常,则必须管理 EXCEPTION_RECORD.ExceptionInformation 数据成员数组中的任何指针所引用的内存。 你必须确保内存在相应的 exception_ptr 对象的生存期内有效,并且在 exception_ptr 对象删除时释放内存。

你可以将结构化异常 (SE) 转换器函数与传输异常功能结合使用。 如果 SEH 异常转换为 C++ 异常,current_exception 函数返回的 exception_ptr 将引用转换后的异常,而不是原始 SEH 异常。 rethrow_exception函数引发转换后的异常,而不是原始异常。 有关 SE 转换器函数的详细信息,请参阅 _set_se_translator

rethrow_exception 函数

exception_ptr 对象中存储捕获的异常后,主线程便可以处理该对象。 在主线程中,调用 rethrow_exception 函数,将 exception_ptr 对象作为其参数。 rethrow_exception 函数从 exception_ptr 对象中提取异常,然后在主线程的上下文中引发异常。 如果 rethrow_exception 函数的 p 参数为 null exception_ptr,则该函数引发 std::bad_exception

提取的异常现在是主线程中的当前异常,因此你可以像处理任何其他异常一样对其进行处理。 如果你捕获异常,可以立即处理它,或使用 throw 语句将它发送到更高级别的异常处理程序。 否则,不执行任何操作并允许默认系统异常处理程序来终止进程。

make_exception_ptr 函数

make_exception_ptr 函数采用类的实例作为其参数,然后返回引用该实例的 exception_ptr。 通常,指定异常类对象作为参数传递给 make_exception_ptr 函数,但任意类对象都可以是参数。

调用 make_exception_ptr 函数等效于引发 C++ 异常、在 catch 块中捕获它并调用 current_exception 函数以返回引用异常的 exception_ptr 对象。 Microsoft 实现的 make_exception_ptr 函数比调用并捕获异常更高效。

应用程序通常无需make_exception_ptr函数,因此,不建议使用此函数。

示例

以下示例从一个线程向另一个线程传输标准 C++ 异常和自定义 C++ 异常。

// transport_exception.cpp
// compile with: /EHsc /MD
#include <windows.h>
#include <stdio.h>
#include <exception>
#include <stdexcept>

using namespace std;

// Define thread-specific information.
#define THREADCOUNT 2
exception_ptr aException[THREADCOUNT];
int           aArg[THREADCOUNT];

DWORD WINAPI ThrowExceptions( LPVOID );

// Specify a user-defined, custom exception.
// As a best practice, derive your exception
// directly or indirectly from std::exception.
class myException : public std::exception {
};
int main()
{
    HANDLE aThread[THREADCOUNT];
    DWORD ThreadID;

    // Create secondary threads.
    for( int i=0; i < THREADCOUNT; i++ )
    {
        aArg[i] = i;
        aThread[i] = CreateThread(
            NULL,       // Default security attributes.
            0,          // Default stack size.
            (LPTHREAD_START_ROUTINE) ThrowExceptions,
            (LPVOID) &aArg[i], // Thread function argument.
            0,          // Default creation flags.
            &ThreadID); // Receives thread identifier.
        if( aThread[i] == NULL )
        {
            printf("CreateThread error: %d\n", GetLastError());
            return -1;
        }
    }

    // Wait for all threads to terminate.
    WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
    // Close thread handles.
    for( int i=0; i < THREADCOUNT; i++ ) {
        CloseHandle(aThread[i]);
    }

    // Rethrow and catch the transported exceptions.
    for ( int i = 0; i < THREADCOUNT; i++ ) {
        try {
            if (aException[i] == NULL) {
                printf("exception_ptr %d: No exception was transported.\n", i);
            }
            else {
                rethrow_exception( aException[i] );
            }
        }
        catch( const invalid_argument & ) {
            printf("exception_ptr %d: Caught an invalid_argument exception.\n", i);
        }
        catch( const myException & ) {
            printf("exception_ptr %d: Caught a  myException exception.\n", i);
        }
    }
}
// Each thread throws an exception depending on its thread
// function argument, and then ends.
DWORD WINAPI ThrowExceptions( LPVOID lpParam )
{
    int x = *((int*)lpParam);
    if (x == 0) {
        try {
            // Standard C++ exception.
            // This example explicitly throws invalid_argument exception.
            // In practice, your application performs an operation that
            // implicitly throws an exception.
            throw invalid_argument("A C++ exception.");
        }
        catch ( const invalid_argument & ) {
            aException[x] = current_exception();
        }
    }
    else {
        // User-defined exception.
        aException[x] = make_exception_ptr( myException() );
    }
    return TRUE;
}
exception_ptr 0: Caught an invalid_argument exception.
exception_ptr 1: Caught a  myException exception.

要求

标头<exception>

另请参阅

异常处理/EH(异常处理模型)
/clr(公共语言运行时编译)