1. 程式人生 > >.NET/C# 異常處理:寫一個空的 try 塊程式碼,而把重要程式碼寫到 finally 中(Constrained Execution Regions)

.NET/C# 異常處理:寫一個空的 try 塊程式碼,而把重要程式碼寫到 finally 中(Constrained Execution Regions)

不知你是否見過 try { } finally { } 程式碼中,try 塊留空,而只往 finally 中寫程式碼的情況呢?這種寫法有其特殊的目的。

本文就來說說這種不一樣的寫法。


本文內容

空的 try 塊

你可以點開這個連結檢視 Exception 類,在裡面你可以看到一段異常處理的程式碼非常奇怪:

// 程式碼已經過簡化。
internal
void RestoreExceptionDispatchInfo(ExceptionDispatchInfo exceptionDispatchInfo) { // 省略程式碼。 try{} finally { // 省略程式碼。 } // 省略程式碼。 }

神奇之處就在於,其 try 塊是空的,重要程式碼都放在 finally 中。那為什麼會這麼寫呢?

在程式碼註釋中的解釋為:

We do this inside a finally clause to ensure ThreadAbort cannot be injected while we have taken the lock. This is to prevent unrelated exception restorations from getting blocked due to TAE.

翻譯過來是:

finally 子句中執行此操作以確保在獲取鎖時無法注入 ThreadAbort。這是為了防止不相關的異常恢復因 TAE 而被阻止。

也就是說,此方法是為了與 Thread.Abort 對抗,防止 Thread.Abort 中斷此處程式碼的執行。 Thread.Abort 的執行交給 CLR 管理,finally 的執行也是交給 CLR 管理。CLR 確保 finally 塊執行的時候不會被 Thread.Abort 阻止。

程式碼在 .NET Core 和 .NET Framework 中的實現完全一樣:

// This is invoked by ExceptionDispatchInfo.Throw to restore the exception stack trace, corresponding to the original throw of the
// exception, just before the exception is "rethrown". [SecuritySafeCritical] internal void RestoreExceptionDispatchInfo(System.Runtime.ExceptionServices.ExceptionDispatchInfo exceptionDispatchInfo) { bool fCanProcessException = !(IsImmutableAgileException(this)); // Restore only for non-preallocated exceptions if (fCanProcessException) { // Take a lock to ensure only one thread can restore the details // at a time against this exception object that could have // multiple ExceptionDispatchInfo instances associated with it. // // We do this inside a finally clause to ensure ThreadAbort cannot // be injected while we have taken the lock. This is to prevent // unrelated exception restorations from getting blocked due to TAE. try{} finally { // When restoring back the fields, we again create a copy and set reference to them // in the exception object. This will ensure that when this exception is thrown and these // fields are modified, then EDI's references remain intact. // // Since deep copying can throw on OOM, try to get the copies // outside the lock. object _stackTraceCopy = (exceptionDispatchInfo.BinaryStackTraceArray == null)?null:DeepCopyStackTrace(exceptionDispatchInfo.BinaryStackTraceArray); object _dynamicMethodsCopy = (exceptionDispatchInfo.DynamicMethodArray == null)?null:DeepCopyDynamicMethods(exceptionDispatchInfo.DynamicMethodArray); // Finally, restore the information. // // Since EDI can be created at various points during exception dispatch (e.g. at various frames on the stack) for the same exception instance, // they can have different data to be restored. Thus, to ensure atomicity of restoration from each EDI, perform the restore under a lock. lock(Exception.s_EDILock) { _watsonBuckets = exceptionDispatchInfo.WatsonBuckets; _ipForWatsonBuckets = exceptionDispatchInfo.IPForWatsonBuckets; _remoteStackTraceString = exceptionDispatchInfo.RemoteStackTrace; SaveStackTracesFromDeepCopy(this, _stackTraceCopy, _dynamicMethodsCopy); } _stackTraceString = null; // Marks the TES state to indicate we have restored foreign exception // dispatch information. Exception.PrepareForForeignExceptionRaise(); } } }

你可以在 這裡 檢視 .NET Framework 版本,在這裡 檢視 .NET Core 的版本。

受約束的執行區域(Constrained Execution Regions)

這種現象在微軟官方文件 可靠性最佳做法 中有介紹。

Doing so instructs the just-in-time compiler to prepare all the code in the finally block before running the try block. This guarantees that the code in the finally block is built and will run in all cases. It is not uncommon in a CER to have an empty try block. Using a CER protects against asynchronous thread aborts and out-of-memory exceptions. See ExecuteCodeWithGuaranteedCleanup for a form of a CER that additionally handles stack overflows for exceedingly deep code.

使用 try-finally 形成一個受約束的執行區域,使得 finally 中的程式碼被可靠地執行。


參考資料