约束执行区域(CER)

作者:追风剑情 发布于:2021-1-14 9:26 分类:C#

许多应用程序都不需要健壮到能从任何错误中恢复的地步。许多客户端应用程序都是这样设计的,比如 Notepad.exe(记事本)和 Calc.exe(计算器)。另外,我们中的许多人都经历过Microsoft Office 应用程序(比如 WinWord.exe, Excel.exe 和 Outlook.exe)因为未处理的异常而终止的情况。此外,许多服务器端应用程序(比如 Web 服务器)都是无状态的,会在因为未处理的异常而失败时自动重启。当然,某些服务器(比如 SQL Server)本来就是为状态管理而设计的。这种程序假如因为未处理的异常而发生数据丢失,后果将是灾难性的。

在 CLR 中,我们有包含了状态的 AppDomain。AppDomain 卸载时,它的所有状态都会卸载。所以,如果 AppDomain 中的一个线程遭遇未处理的异常,可以在不终止整个进程的情况下卸载 AppDomain(会销毁它的所有状态)①。

①如果线程的整个生命期都在一个 AppDomain 的内部(比如 ASP.NET 和托管 SQL Server 存储过程),这个说法是完全成立的。但如果线程在其生存期内跨越了 AppDomain 边界,可能就不得不终止整个进程了。

根据定义,CER 是必须对错误有适应力的代码块。由于 AppDomain 可能被卸载,造成它的状态被销毁,所以一般用 CER 处理由多个 AppDomain 或进程共享的状态。如果要在抛出了非预期的异常时维护状态,CER 就非常有用。有时将这些异常称为异步异常。例如,调用方法时,CLR 必须加载一个程序集,在 AppDomain 的 Loader 堆中创建类型对象,调用类型的静态构造器,并将IL代码 JIT 编译成本机代码。所有这些操作都可能失败,CLR通过抛出异常来报告失败。

如果任何这些操作在一个 catch 或 finally 块中失败,你的错误恢复或资源清理代码就不会完整地执行。下例演示了可能出现的问题:

private static void Demo1() {
	try {
		Console.WriteLine("In try");
	}
	finally {
		//隐式调用Type1的静态构造器
		Type1.M();
	}
}

private sealed class Type1 {
	static Type1() {
		//如果这里抛出异常,M就得不到调用
		Console.WriteLine("Type1's static ctor called");
	}
	
	public static void M() {}
}

//运行这个版本得到以下输出:
//In try

我们想要达到的目的是,除非保证(或大致保证)关联的 catch 和 finally 块中的代码得到执行,否则上述 try 块中的代码根本不要开始执行。为了达到这个目的,可以像下面这样修改代码:

①“保证”和“大致保证”分别对应后文所说的 WillNotCorruptState 和 MayCorruptInstance 两个校举成员。——译注

private static void Demo2() {
	//强迫finally块中的代码提前准备好
	//System.Runtime.CompilerServices命名空间
	RuntimeHelpers.PrepareConstrainedRegions();
	try {
		Console.WriteLine("In try");
	}
	finally {
		//隐式调用Type1的静态构造器
		Type2.M();
	}
}

private sealed class Type2 {
	static Type2() {
		//如果这里抛出异常,M就得不到调用
		Console.WriteLine("Type1's static ctor called");
	}
	
	//应用在System.Runtime.ConstrainedExecution命名空间中定义的这个特性
	[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
	public static void M() {}
}

//运行这个版本得到以下输出:
//Type2's static ctor called
//In try

PrepareConstrainedRegions 是一个很特别的方法。JIT 编译器如果发现在一个 try 块之前调用了这个方法,就会提前编译与 try 关联的 catch 和 finally 块中的代码。JTT 编译器会加载任何程序集,创建任何类型对象,调用任何静态构造器,并对任何方法进行 JIT 编译。如果其中任何操作造成异常,这个异常会在线程进入 try 块之前发生。

JT编译器提前准备方法时,还会遍历整个调用图,提前准备被调用的方法,前提是这些方法应用了 ReliabilityContractAttribute,而且向这个特性实例的构造器传递的是Consistency.WillNotCorruptState 或者 Consistency.MayCorruptInstance 枚举成员。这是由于假如方法会损坏 AppDomain 或进程的状态,CLR 便无法对状态一致性做出任何保证。在通过一个 PrepareConstrainedRegions 调用来保护的一个 catch 或 finally 块中,请确保只调用根据刚才的描述设置了 ReliabilityContractAttribute 的方法。

ReliabilityContractAttribute 是像下面这样定义的:

public sealed class ReliabilityContractAttribute : Attribute {
	public ReliabilityContractAttribute(Consistency consistencyGuarantee, Cer cer);
	public Cer Cer { get; }
	public Consistency ConsistencyGuarantee { get; }
}

该特性允许开发者向方法(也可将这个特性应用于接口、构造器、结构、类或者程序集)的潜在调用者申明方法的可靠性协定(reliability contract), Cer和 Consistency 都是枚举类型,它们的定义如下:

enum Consistency {
	MayCorruptProcess, MayCorruptAppDomain, MayCorruptInstance, WillNotCorruptState
}
enum Cer { None, MayFail, Success )

如果你写的方法保证不损坏任何状态,就用 Consistency.WillNotCorruptState,否则就用其他三个值之一来申明方法可能损坏哪一种状态。如果方法保证不会失败,就用Cer.Success,否则用 Cer.MayFail。没有应用 ReliabilityContractAttribute 的任何方法等价于像下面这样标记:
[ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)]

Cer.None 这个值表明方法不进行 CER 保证。换言之,方法没有 CER 的概念。因此,这个方法可能失败,而且可能会、也可能不会报告失败。记住,大多数这些设置都为方法提供了一种方式来申明它向潜在的调用者提供的东西,使调用者知道什么可以期待。CLR和JIT编译器不使用这种信息。

如果想写一个可靠的方法,务必保持它的短小精悍,同时约束它做的事情。要保证它不分配任何对象(例如不装箱)。另外,不调用任何虚方法或接口方法,不使用任何委托,也不使用反射,因为 JIT 编译器不知道实际会调用哪个方法。然而,可以调用 RuntimeHelper类定义的以下方法之一,从而手动准备这些方法:
public static void PrepareMethod(RuntimeMethodHandle method)
public static void PrepareMethod(RuntimeMethodHandle method,
  RuntimeTypeHandle[] instantiation)
public static void PrepareDelegate(Delegate d);
public static void PrepareContractedDelegate(Delegate d);

注意,编译器和 CLR 并不验证你写的方法真的符合通过 ReliabilityContractAttribute来作出的保证。所以,如果犯了错误,状态仍有可能损坏。

注意  即使所有方法都提前准备好,方法调用仍有可能造成 StackOverflowException。在CLR没有寄宿的前提下,StackOverflowException会造成 CLR 在内部调用 Environment.FailFast 来立即终止进程。在已经寄宿的前提下, PrepareConstrainedRegions 方法检查是否剩下约 48 KB的栈空间。栈空间不足,就在进入 try 块前抛出 StackOverflowException。

还应该关注一下 RuntimeHelper 的 ExecuteCodeWithGuaranteedCleanup 方法,它在资源保证得到清理的前提下才执行代码:
public static void ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData);

调用这个方法时,要将try块和finally块的主体作为回调方法传递,它们的原型分别匹配以下两个委托:
public delegate void TryCode(Object userData);
public delegate void CleanupCode(Object userData, Boolean exceptionTrown);

最后,另一种保证代码得以执行的方式是使用CriticalFinalizerObject类,该类将在 托管堆和垃圾回收 详细解释。

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号