可靠性最佳做法

以下可靠性规则面向 SQL Server;但是,它们也适用于任何基于主机的服务器应用程序。 SQL Server 等服务器不会泄漏资源,并且不会关闭,这一点非常重要。 但是,这不能通过为更改对象状态的每个方法编写退出代码来完成。 目标不在于编写出将结合退出代码从每个位置的错误进行恢复的 100% 可靠的托管代码。 这将是一项艰巨的任务,几乎没有成功的机会。 公共语言运行时(CLR)无法轻松为托管代码提供足够强大的保证,使编写完美的代码可行。 请注意,与 ASP.NET 不同的是,SQL Server 仅使用一个进程,而要回收这一进程则需要长时间关闭数据库,这是不可接受的。

由于这些较弱的保证并且在单个进程中运行,可靠性基于在必要时终止线程或回收应用程序域,同时采取预防措施确保不会泄漏操作系统资源(如句柄或内存)。 即使这种更简单的可靠性约束,仍存在重要的可靠性要求:

  • 决不泄漏操作系统资源。

  • 识别针对 CLR 的各种形式的所有托管锁。

  • 切勿中断跨应用程序域共享状态,使 AppDomain 回收能够顺利运行。

尽管从理论上讲,可以编写托管代码来处理ThreadAbortExceptionStackOverflowExceptionOutOfMemoryException异常,但期望开发人员在整个应用程序中编写如此可靠的代码是不合理的。 因此,带外异常会导致执行线程被终止;如果该被终止的线程正在编辑共享状态——这可以通过判断线程是否持有锁来确定——那么 AppDomain 也将被卸载。 当编辑共享状态的方法终止时,该状态将损坏,因为无法为共享状态的更新编写可靠的回退代码。

在 .NET Framework 版本 2.0 中,唯一需要可靠性的主机是 SQL Server。 如果程序集将在 SQL Server 上运行,则应为该程序集的每个部分执行可靠性工作,即使数据库中运行时禁用了特定的功能。 这是必需的,因为代码分析引擎检查程序集级别的代码,并且无法区分禁用的代码。 另一个 SQL Server 编程注意事项是,SQL Server 在单个进程中运行所有内容,并通过AppDomain回收来清理所有资源,例如内存和操作系统句柄。

不能依赖终结器、析构函数或 try/finally 块来执行退出代码。 这些功能可能会被中断或是未被调用。

异步异常可能会在意想不到的位置被抛出,甚至可能在每个机器指令中出现:ThreadAbortExceptionStackOverflowExceptionOutOfMemoryException

托管线程不一定是 SQL 中的 Win32 线程;它们可能是纤维。

进程范围或跨应用程序域可变共享状态极难安全更改,应尽可能避免。

SQL Server 中内存不足的情况并不罕见。

如果托管在 SQL Server 中的库未正确更新其共享状态,则代码在重新启动数据库之前不会恢复的可能性很高。 此外,在某些情况下,可能会导致 SQL Server 进程失败,从而导致数据库重新启动。 重新启动数据库可能会关闭网站或影响公司运营,从而损害可用性。 操作系统资源(如内存或句柄)的泄漏缓慢可能导致服务器最终无法分配句柄,并且无法恢复,或者可能会导致服务器性能逐渐下降,进而降低客户应用程序的可用性。 显然,我们希望避免这些方案。

最佳做法规则

简介重点介绍服务器中运行的托管代码的代码评审需要捕捉的内容,以提高框架的稳定性和可靠性。 所有这些检查一般情况下都是很好的做法,绝对必须在服务器上进行。

面对死锁或资源约束时,SQL Server 将中止线程或关闭 AppDomain。 在此情况下,只能保证运行受约束的执行区域 (CER) 中的退出代码。

使用 SafeHandle 避免资源泄漏

AppDomain 卸载的情况下,无法依赖正在被执行的 finally 块或终结器,因此,通过 SafeHandle 类(而非 IntPtrHandleRef 或相似的类)抽象所有操作系统资源访问是很重要的。 这样使 CLR 能够跟踪和关闭你使用的句柄,即使在 AppDomain 关闭的情况下也不例外。 SafeHandle 将使用 CLR 将始终运行的一个关键终结器。

操作系统句柄从创建那一刻起一直存储在安全句柄中,直到释放。 在此期间不会出现发生 ThreadAbortException 以泄露句柄的情况。 此外,平台调用将引用计数句柄,这样可以关闭对句柄生存期的跟踪,防止 Dispose 和正在使用句柄的方法之间出现争用条件的安全问题。

目前具有终结器以简单清理操作系统句柄的大多数类将不再需要终结器了。 相反,终结器将位于 SafeHandle 派生类上。

请注意, SafeHandle 不是替代项 IDisposable.Dispose。 显示释放操作系统资源仍然有潜在的资源争用和性能优势。 但要知道显示释放资源的 finally 块可能不会执行到完成。

SafeHandle 允许你实现自己的 ReleaseHandle 方法来执行释放句柄的工作,例如将状态传递给作系统句柄释放例程或在循环中释放一组句柄。 CLR 保证此方法会被执行。 在任何情况下,实现 ReleaseHandle 以确保句柄被释放是创建者的责任。 如果不能做到这点将导致句柄泄露,这通常会导致与句柄相关的本机资源泄露。 因此,构建 SafeHandle 派生类至关重要,这样 ReleaseHandle 实现就不需要分配在调用时可能不可用的任何资源。 请注意,调用在实现 ReleaseHandle 中可能失败的方法是允许的,只要你的代码可以处理此类失败并且完成协议以释放本机句柄即可。 出于调试目的,ReleaseHandleBoolean 返回值在遇到灾难性错误时可能会设置为 false,以防止资源被释放。 这样做将激活 releaseHandleFailed MDA(如果已启用)以帮助识别问题。 它不会以其他任何方式影响运行时;ReleaseHandle 将不会为同一资源再次被调用,并且因此将导致句柄泄露。

SafeHandle 在某些上下文中不适用。 由于可以在 ReleaseHandle 终结器线程上运行 GC 方法,因此,不应将任何需要在特定线程上释放的句柄包装在 SafeHandle 中。

运行时可调用包装器(RCWs)可以由 CLR 清理,而无需额外的代码。 对于使用平台调用并将 COM 对象视为 IUnknown*IntPtr 的代码,应将代码重写以使用 RCW。 由于非托管的释放方法可能回调到托管代码中,因此 SafeHandle 可能不适用于此方案。

代码分析规则

使用SafeHandle来封装操作系统资源。 请勿使用 HandleRef 或类型为 IntPtr字段。

确保不需要运行终结器即可防止操作系统资源泄露

仔细检查终结器以确保即使它们不运行,关键的操作系统资源也不会泄露。 与应用程序处于稳定状态或 SQL Server 等服务器关闭时的正常 AppDomain 卸载不同,在突然 AppDomain 卸载期间,对象不会被终结。 确保在突然卸载的情况下不会泄漏资源,因为无法保证应用程序的正确性,但服务器的完整性必须通过不泄漏资源来维护。 使用 SafeHandle 来释放任何操作系统资源。

确保不需要运行 finally 子句即可防止操作系统资源泄露

由于不保证 finally 子句可以在 CER 外部运行,因此要求库开发人员不要依赖 finally 块中的代码来释放非托管资源。 使用 SafeHandle 是建议的解决方案。

代码分析规则

使用SafeHandle来清理操作系统资源,而不是Finalize。 请勿使用 IntPtr;请使用 SafeHandle 封装资源。 如果 finally 子句必须运行,请将其放在 CER 中。

所有锁必须遍历现有的托管锁定代码

CLR 必须知道代码何时处于锁中,这样它就会知道要拆解 AppDomain 线程,而不仅仅是中止线程。 中止线程可能很危险,因为线程处理的数据可能会处于不一致状态。 因此,必须回收整个 AppDomain。 未能成功识别锁可能导致死锁或者出现不正确的结果。 使用方法 BeginCriticalRegionEndCriticalRegion 标识锁区域。 它们是仅适用于当前线程的类上的 Thread 静态方法,有助于防止一个线程编辑另一个线程的锁计数。

EnterExit 内置有此 CLR 通知,所以建议使用它们,并且也建议采用使用这些方法的 lock 语句

其他锁定机制(如旋转锁和 AutoResetEvent)必须调用这些方法才能对 CLR 发出正在进入临界区的通知。 这些方法不采用任何锁;它们通知 CLR 代码正在关键部分执行,中止线程可能会使共享状态不一致。 如果已定义自己的锁类型(如自定义 ReaderWriterLock 类),请使用这些锁计数方法。

代码分析规则

使用 BeginCriticalRegionEndCriticalRegion 标记和标识所有锁。 请勿在循环中使用 CompareExchangeIncrementDecrement。 不要对这些方法的 Win32 变体执行平台调用。 不要在循环中使用Sleep。 不要使用可变字段。

清理代码必须在 finally 或 catch 块中,不能位于 catch 之后

清理代码不应出现在 catch 块之后;它应位于 finally 块或 catch 块内。 这应该是一种正常的良好做法。 通常首选使用 finally 块,因为在引发异常时以及在一般情况下达到 try 块的末尾时它都运行相同的代码。 在发生意外异常(例如 ThreadAbortException)时,清理代码将不会执行。 理想情况下,您应该将需要在finally中清理的任何非托管资源封装在SafeHandle中,以防止泄漏。 请注意,可以有效地使用 C# using 键释放对象,包括句柄。

尽管 AppDomain 回收可以清理终结器线程上的资源,但仍有必要将清理代码放在正确的位置。 请注意,如果线程收到异步异常而不持有锁,CLR 会尝试直接结束线程,而无需回收 AppDomain。 尽早清理资源有助于释放更多资源,并更好地管理资源的生命周期。 如果不显示关闭指向某些错误代码路径中的文件的句柄,并且等待 SafeHandle 终结器进行清理,那么下一次你的代码运行时,如果终结器尚未运行,代码尝试访问同一文件可能会失败。 因此,确保清理代码存在且正常工作将有助于更干净、更快速地从故障中恢复,即使它并非严格必要。

代码分析规则

catch 后面的清理代码需要在 finally 块中。 将要进行释放的调用放置在 finally 块中。 catch 块应以引发或再次引发结束。 虽然会有例外情况(例如,检测网络连接是否可以建立的代码时可能会遇到许多异常),但任何需要在正常情况下捕获多个异常的代码都应标明该代码应该经过测试以确定其能否成功。

应消除应用程序域之间的可变共享状态或使用受限的执行区域 Process-Wide

如简介中所述,很难编写托管代码,以可靠的方式监视跨应用程序域的进程范围的共享状态。 进程范围的共享状态是应用程序域之间共享的任何类型的数据结构,无论是在 Win32 代码中、CLR 内部还是在使用远程处理的托管代码中共享。 在托管代码中,任何可变共享状态都很难正确编写,而任何静态共享状态也需要非常谨慎地处理。 如果你有进程范围的或计算机范围的共享状态,请找到一些方法来消除它或使用受限的执行区域(CER)保护共享状态。 请注意,任何具有未识别和更正的共享状态的库都可能导致需要干净卸载的主机(如 SQL Server)崩溃。

如果代码使用 COM 对象,请避免在应用程序域之间共享该 COM 对象。

锁在进程范围内或应用程序域之间不起作用。

过去,人们使用 Enterlock 语句 来创建全局进程锁。 例如,在对 AppDomain 敏捷类进行锁定时会发生这种情况,例如来自非共享程序集的 Type 实例、Thread 对象、暂存的字符串以及某些使用远程处理跨应用程序域共享的字符串。 这些锁不再属于进程范围。 若要确定进程范围的应用域锁的存在,请确定锁中的代码是否使用任何外部持久化资源,例如磁盘上的文件或数据库。

请注意,如果受保护的代码使用外部资源,则锁定 AppDomain 可能会导致问题,因为该代码可能在多个应用程序域中同时运行。 在写入到一个日志文件或绑定到整个进程的套接字时,这可能是个问题。 这些更改意味着除了使用命名的 MutexSemaphore 实例外,没有简单的方法使用托管代码来获取全局进程的锁。 创建不同时在两个应用程序域中运行的代码,或使用 MutexSemaphore 类。 如果无法更改现有代码,请不要使用 Win32 命名的互斥以实现此同步,因为在纤程模式中运行意味着无法保证同一操作系统线程将获取并释放互斥。 必须使用托管 Mutex 类、命名的 ManualResetEventAutoResetEvent,或 Semaphore,以 CLR 能识别的方式同步代码锁,而不是使用非托管代码进行同步。

避免 lock(typeof(MyType))

仅具有一个跨所有应用程序域共享的代码副本的共享程序集中的私有和公用 Type 对象也存在问题。 对于共享程序集,每个进程只有一个 Type 实例,这意味着多个应用程序域共享完全相同 Type 的实例。 在 Type 实例上采用锁是采用影响整个进程而不仅仅是影响 AppDomain 的锁。 如果一个 AppDomainType 对象上采用锁,接着该线程突然被中止,那么它将不会释放锁。 然后,此锁可能会导致其他应用程序域死锁。

在静态方法中获取锁的好方法包括向代码添加静态内部同步对象。 如果存在类构造函数,则可以在其中进行初始化;如果不存在,则可以这样初始化:

private static Object s_InternalSyncObject;
private static Object InternalSyncObject
{
    get
    {
        if (s_InternalSyncObject == null)
        {
            Object o = new Object();
            Interlocked.CompareExchange(
                ref s_InternalSyncObject, o, null);
        }
        return s_InternalSyncObject;
    }
}

然后在获取锁时,使用 InternalSyncObject 属性获取要锁定的对象。 如果在类构造函数中初始化了内部同步对象,则无需使用该属性。 双重检查锁初始化代码应如以下示例所示:

public static MyClass SingletonProperty
{
    get
    {
        if (s_SingletonProperty == null)
        {
            lock(InternalSyncObject)
            {
                // Do not use lock(typeof(MyClass))
                if (s_SingletonProperty == null)
                {
                    MyClass tmp = new MyClass(…);
                    // Do all initialization before publishing
                    s_SingletonProperty = tmp;
                }
            }
        }
        return s_SingletonProperty;
    }
}

有关 Lock(this) 的说明

通常来讲,对一个可以被公开访问的独立对象加锁是可以接受的。 但是,如果对象是可能导致整个子系统死锁的单例对象,请考虑使用上述设计模式。 例如,锁定一个SecurityManager对象可能会导致AppDomain中出现死锁,使整个AppDomain无法使用。 最好不要锁定此类型的可公开访问对象。 但是,单个集合或数组上的锁通常不应出现问题。

代码分析规则

不要对可能在不同应用程序域中使用的类型加锁,或对那些缺乏明确标识的类型加锁。 不要在EnterTypeMethodInfoPropertyInfoStringValueType或任何派生自Thread的对象上调用MarshalByRefObject

删除 GC.KeepAlive 调用

现有大量代码要么在应该使用 KeepAlive 时没有使用,要么在不合适的时候使用了它。 转换为 SafeHandle后,类不需要调用 KeepAlive,假设它们没有终结器,而是依赖于 SafeHandle 来完成操作系统句柄的管理。 尽管保留调用 KeepAlive 的性能成本可能微不足道,但认为调用 KeepAlive 是解决可能已经不存在的生命周期问题所必需或足够的,这种认知使得代码更难维护。 但是,当使用 COM 互操作 CLR 可调用包装器 (RCW) 时,代码还是需要 KeepAlive

代码分析规则

删除 KeepAlive

使用 HostProtection 属性

HostProtectionAttribute (HPA) 允许使用声明性安全操作来决定主机保护需求,使主机甚至能够阻止完全信任的代码调用不适用于给定主机的某些方法,例如针对 SQL Server 的 ExitShow

HPA 仅影响承载公共语言运行时并实现主机保护(例如 SQL Server)的没有管理功能的应用程序。 应用安全操作后,将根据类或方法公开的主机资源创建链接需求。 如果代码在客户端应用程序中或在不受主机保护的服务器上运行,则属性“蒸发”;它未检测到,因此未应用。

重要

此属性的目的是强制实施特定于主机的编程模型准则,而不是安全行为。 尽管链接需求用于检查是否符合编程模型要求,但 HostProtectionAttribute 这不是安全权限。

如果主机没有编程模型要求,则不会发生链接要求。

此属性标识以下内容:

  • 不适合主机编程模型的方法或类,但本来是良性的。

  • 不适合主机编程模型的方法或类,可能会导致服务器托管的用户代码不稳定。

  • 不适合主机编程模型的方法或类,可能会导致服务器进程本身的不稳定。

注释

如果要创建可由可在主机保护环境中执行的应用程序调用的类库,则应将此属性应用于公开 HostProtectionResource 资源类别的成员。 具有此属性的 .NET Framework 类库成员只会导致直接调用方被检查。 你的库成员也必须使用同一方法对其直接调用方进行检查。

有关 HPA 的详细信息,请参阅 HostProtectionAttribute

代码分析规则

对于 SQL Server,用于引入同步或线程处理的所有方法都必须使用 HPA 进行标识。 这包括共享状态、同步或管理外部进程的方法。 HostProtectionResource影响 SQL Server 的值是SharedStateSynchronization以及ExternalProcessMgmt。 但是,任何暴露任何 HostProtectionResource 的方法都应由 HPA 标识,而不仅仅是那些使用影响 SQL 的资源的方法。

不要在非托管代码中无限期阻塞

在非托管代码中而不是在托管代码中阻塞可能导致拒绝服务攻击,因为 CLR 无法中止线程。 已阻塞的线程会阻止 CLR 卸载 AppDomain,至少是在没有执行某些极端不安全操作的情况下。 使用 Windows 同步基元进行阻塞是我们不允许的一个明显示例。 应尽量避免在套接字上对 ReadFile 的调用中阻塞 — 理想情况下,Windows API 应为此类似的操作提供超时的机制。

理想情况下,任何调入本机的方法应使用具有合理、有限的超时的 Win32 调用。 如果允许用户指定超时,则不应允许用户在没有某些特定安全权限的情况下指定无限超时。 按照一般准则,如果方法将阻塞超过 10 秒,你则需要使用支持超时的版本,或需要其他的 CLR 支持。

下面是一些有问题的 API 的示例。 虽然在超时的情况下可以创建管道(匿名和命名管道皆可);但是,代码必须确保它永不使用 NMPWAIT_WAIT_FOREVER 调用 CreateNamedPipeWaitNamedPipe。 此外,即使指定了超时,也有可能发生意外的阻塞。 在匿名管道上调用 WriteFile 将会在全部字节被写入之前阻塞,这意味着如果缓冲区在其中有未读数据,那么在读取器释放管道缓冲区中的空间之前,WriteFile 调用将阻塞。 套接字应该始终使用一些提供超时机制的 API。

代码分析规则

在非托管代码中在没有超时的情况下进行阻塞是拒绝服务攻击。 不要执行对 WaitForSingleObject、、 WaitForSingleObjectExWaitForMultipleObjectsMsgWaitForMultipleObjectsMsgWaitForMultipleObjectsEx的平台调用。 请勿使用NMPWAIT_WAIT_FOREVER。

识别任何依赖 STA 的功能

识别任何使用 COM 单线程单元 (STA) 的代码。 在 SQL Server 进程中禁用 STA。 必须在 SQL Server 中禁用依赖 CoInitialize的功能,例如性能计数器或剪贴板。

确保终结器不存在同步问题

.NET Framework 的未来版本中可能存在多个终结器线程,这意味着同一类型的不同实例的终结器同时运行。 它们不必完全线程安全;垃圾回收器保证只有一个线程将运行给定对象实例的终结器。 但是,必须对终结器进行编码以避免同时在多个不同的对象示例上运行时出现争用条件和死锁的情况。 在终结器中使用任何外部状态(例如写入日志文件)时,必须处理线程问题。 不要依赖最终化来提供线程安全性。 不要使用线程本地存储(托管或本机)在终结器线程上存储状态。

代码分析规则

终结器不得存在同步问题。 请勿在终结器中使用静态可变状态。

如果可能,请避免不受管理的内存

非托管内存可能会被泄露,正如操作系统句柄一样。 如有可能,请使用 stackalloc 或固定的托管对象(如 fixed 语句或使用 byte[] 的 GCHandle)在堆栈上尝试使用内存。 GC 最终会清理这些内容。 但是,如果必须分配非托管内存,请考虑使用派生自 SafeHandle 的类来包装内存分配。

请注意,至少存在一种 SafeHandle 不适用的情况。 对于分配或释放内存的 COM 方法调用,通常由一个 DLL 通过 CoTaskMemAlloc 分配内存,然后另一个 DLL 通过 CoTaskMemFree 释放该内存。 在这些位置使用 SafeHandle 是不合适的,因为它会尝试将非托管内存的生存期绑定到 SafeHandle 的生存期,而不是允许其他 DLL 控制内存的生存期。

查看 catch(Exception) 的所有用法

捕获所有异常而不是捕获某个特定异常的 catch 块现在也将捕获异步异常。 检查每个 catch(Exception) 块,以确认没有重要的资源释放、可能被跳过的退出代码以及用于处理 ThreadAbortExceptionStackOverflowExceptionOutOfMemoryException 的 catch 块自身中潜在的不正确行为。 请注意,此代码可能是日志记录或做出一些假设,即它可能只看到某些异常,或者每当发生异常时,它就会因为一个特定原因而失败。 可能需要更新这些假设才能包括 ThreadAbortException

请考虑更改捕获所有异常的所有位置以捕获期待将引发的特定类型的异常,例如来自字符串格式化方法的 FormatException。 这可以防止捕获块在意外异常时运行,并有助于确保代码不会通过捕获意外异常来隐藏漏洞。 一般规则永远不会处理库代码中的异常(要求捕获异常的代码可能表示所调用的代码中存在设计缺陷)。 在某些情况下,你可能想捕获异常并且引发不同的异常类型以提供更多的数据。 在这种情况下,请使用嵌套异常,将失败 InnerException 的真正原因存储在新异常的属性中。

代码分析规则

查看托管代码中的所有 catch 块,这些块捕获所有对象或捕获所有异常。 在 C# 中,这意味着同时标记 catch{} 和 catch(Exception){}。 请考虑使异常类型非常具体,或查看代码,以确保它在捕获意外异常类型时不会以错误的方式运行。

不要假设托管线程是 Win32 线程 - 它是光纤

使用托管线程本地存储可以正常工作,但不能使用非托管线程本地存储,或者假定代码将再次在当前作系统线程上运行。 不要更改设置,如线程的区域设置。 不要通过平台调用InitializeCriticalSectionCreateMutex ,因为它们要求进入锁的操作系统线程也必须退出锁。 由于使用纤程时将不存在此问题,所以不能直接在 SQL 中使用 Win32 临界区和互斥。 请注意,托管 Mutex 类不处理这些线程相关性问题。

可以在托管 Thread 对象上安全使用大部分状态,包括托管线程本地存储和线程当前的 UI 区域性。 也可以使用 ThreadStaticAttribute,这使得现有静态变量的值只能由当前托管线程访问(这是在 CLR 中执行光纤本地存储的另一种方法)。 出于编程模型的原因,在 SQL 中运行时不能更改线程当前的区域性。

代码分析规则

SQL Server 在光纤模式下运行;不使用线程本地存储。 请避免执行对 TlsAllocTlsFreeTlsGetValueTlsSetValue. 的平台调用

让 SQL Server 处理模拟

由于模拟在线程级别运行,SQL 可以在光纤模式下运行,因此托管代码不应模拟用户,也不应调用 RevertToSelf

代码分析规则

让 SQL Server 处理身份模拟。 请勿使用RevertToSelfImpersonateAnonymousTokenDdeImpersonateClientImpersonateDdeClientWindowImpersonateLoggedOnUserImpersonateNamedPipeClientImpersonateSelfRpcImpersonateClientRpcRevertToSelfRpcRevertToSelfExSetThreadToken

不要调用 Thread::Suspend

挂起线程的功能可能在一个简单的操作中实现,但是它可能会导致死锁。 如果持有锁的线程被第二个线程暂停,并且第二个线程尝试获取同一锁,则会发生死锁。 Suspend 当前会对安全性、类加载、远程处理和反射造成干扰。

代码分析规则

请勿调用 Suspend。 请考虑改用实际同步基元,例如 SemaphoreManualResetEvent

使用受限执行区和可靠性契约保护关键操作

在执行更新共享状态或需要确定性完全成功或完全失败的复杂操作时,请确保它受约束执行区域(CER)的保护。 这确保代码在任何情况下都能运行,即使是突然的线程中止或突然的卸载操作。

一个 CER 是 try/finally 调用后面紧跟的一个特定 PrepareConstrainedRegions 块。

这样做会指示实时编译器在运行 try 块之前准备最后一个块中的所有代码。 这可以保证最终块中的代码生成,并将在所有情况下运行。 在 CER 中,有一个空 try 块并不罕见。 使用 CER 可防止异步线程中止和内存不足异常。 请参阅 ExecuteCodeWithGuaranteedCleanup 一种 CER 形式,该 CER 可额外处理堆栈溢出,以处理非常深入的代码。

另请参阅