Jeffrey Richter here again. Today I’d like to share one more section from my latest book. It’s from Chapter 20, “Exceptions and State Management.” You can search this blog for more excerpts from the book. Thanks for your interest!
Constrained Execution Regions (CERs)
Many applications don’t need to be robust and recover from any and all kinds of failures. This is true of many client applications like Notepad.exe and Calc.exe. And, of course, many of us have seen Microsoft Office applications like WinWord.exe, Excel.exe, and Outlook.exe terminate due to unhandled exceptions. Also, many server-side applications, like Web servers, are stateless and are automatically restarted if they fail due to an unhandled exception. Of course some servers, like SQL Server, are all about state management and having data lost due to an unhandled exception is potentially much more disastrous.
In the CLR, we have AppDomains (discussed in Chapter 22), which contain state. When an AppDomain is unloaded, all its state is unloaded. And so, if a thread in an AppDomain experiences an unhandled exception, it is OK to unload the AppDomain (which destroys all its state) without terminating the whole process.8
By definition, a CER is a block of code that must be resilient to failure. Since AppDomains can be unloaded, destroying their state, CERs are typically used to manipulate any state that is shared by multiple AppDomains or processes. CERs are useful when trying to maintain state in the face of exceptions that get thrown unexpectedly. Sometimes we refer to these kinds of exceptions as asynchronous exceptions. For example, when calling a method, the CLR has to load an assembly, create a type object in the AppDomain’s loader heap, call the type’s static constructor, JIT IL into native code, and so on. Any of these operations could fail, and the CLR reports the failure by throwing an exception.
If any of these operations fail within a catch or finally block, then your error recovery or cleanup code won’t execute in its entirety. Here is an example of code that exhibits the potential problem:
private static void Demo1() { try { Console.WriteLine("In try"); } finally { // Type1’s static constructor is implicitly called in here Type1.M(); } } private sealed class Type1 { static Type1() { // if this throws an exception, M won’t get called Console.WriteLine("Type1's static ctor called"); } public static void M() { } }
When I run the code above, I get the following output:
In try Type1's static ctor called
What we want is to not even start executing the code in the try block above unless we know that the code in the associated catch and finally blocks is guaranteed (or as close as we can get to guaranteed) to execute. We can accomplish this by modifying the code as follows:
private static void Demo2() { // Force the code in the finally to be eagerly prepared RuntimeHelpers.PrepareConstrainedRegions(); // System.Runtime.CompilerServices namespace try { Console.WriteLine("In try"); } finally { // Type2’s static constructor is implicitly called in here Type2.M(); } } public class Type2 { static Type2() { Console.WriteLine("Type2's static ctor called"); } // Use this attribute defined in the System.Runtime.ConstrainedExecution namespace [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public static void M() { } }
Now, when I run this version of the code, I get the following output:
Type2's static ctor called In try
The PrepareConstrainedRegions method is a very special method. When the JIT compiler sees this method being called immediately before a try block, it will eagerly compile the code in the try’s catch and finally blocks. The JIT compiler will load any assemblies, create any type objects, invoke any static constructors, and JIT any methods. If any of these opera- tions result in an exception, then the exception occurs before the thread enters the try block.
When the JIT compiler eagerly prepares methods, it also walks the entire call graph eagerly preparing called methods. However, the JIT compiler only prepares methods that have the ReliabilityContractAttribute applied to them with either Consistency. WillNotCorruptState or Consistency.MayCorruptInstance because the CLR can’t make any guarantees about methods that might corrupt AppDomain or process state. Inside a catch or finally block that you are protecting with a call to PrepareConstrainedRegions, you want to make sure that you only call methods with the ReliabillityContractAttribute set as I’ve just described.
The ReliabilityContractAttribute looks like this:
public sealed class ReliabilityContractAttribute : Attribute { public ReliabilityContractAttribute(Consistency consistencyGuarantee, Cer cer); public Cer Cer { get; } public Consistency ConsistencyGuarantee { get; } }
This attribute lets a developer document the reliability contract of a particular method9 to the method’s potential callers. Both the Cer and Consistency types are enumerated types defined as follows:
enum Consistency { MayCorruptProcess, MayCorruptAppDomain, MayCorruptInstance, WillNotCorruptState } enum Cer { None, MayFail, Success }
If the method you are writing promises not to corrupt any state, use Consistency. WillNotCorruptState. Otherwise, document what your method does by using one of the other three possible values that match whatever state your method might corrupt. If the method that you are writing promises not to fail, use Cer.Success. Otherwise, use Cer. MayFail. Any method that does not have the ReliabilityContractAttribute applied to it is equivalent to being marked like this:
[ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)]
The Cer.None value indicates that the method makes no CER guarantees. In other words, it wasn’t written with CERs in mind; therefore, it may fail and it may or may not report that it failed. Remember that most of these settings are giving a method a way to document what it offers to potential callers so that they know what to expect. The CLR and JIT compiler do not use this information.
When you want to write a reliable method, make it small and constrain what it does. Make sure that it doesn’t allocate any objects (no boxing, for example), don’t call any virtual meth- ods or interface methods, use any delegates, or use reflection because the JIT compiler can’t tell what method will actually be called. However, you can manually prepare these methods by calling one of these methods defined by the RuntimeHelpers’s class:
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);
Note that the compiler and the CLR do nothing to verify that you’ve written your method to actually live up to the guarantees you document via the ReliabiltyContractAttribute. If you do something wrong, then state corruption is possible.
Note Even if all the methods are eagerly prepared, a method call could still result in a StackOverflowException. When the CLR is not being hosted, a StackOverflowException causes the process to terminate immediately by the CLR internally calling Environment. FailFast. When hosted, the PreparedConstrainedRegions method checks the stack to see if there is approximately 48KB of stack space remaining. If there is limited stack space, the StackOverflowException occurs before entering the try block.
You should also look at RuntimeHelper’s ExecuteCodeWithGuaranteedCleanup method which is another way to execute code with guaranteed cleanup:
public static void ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData);
When calling this method, you pass the body of the try and finally block as callback methods whose prototypes match these two delegates respectively:
public delegate void TryCode(Object userData); public delegate void CleanupCode(Object userData, Boolean exceptionThrown);
And finally, another way to get guaranteed code execution is to use the CriticalFinalizerObject class which is explained in great detail in Chapter 21.