之前只知道在同步方法中調用異步(async)方法時,如果用.Result等待調用結果,會造成線程死鎖(deadlock)。自己也吃過這個苦頭,詳見等到花兒也謝了的await。
昨天一個偶然的情況,造成在同步方法中調用了async方法,并且沒有使用.Result,結果造成整個ASP.NET應用程序的崩潰,見識了同步/異步水火難容的厲害。
當時的情況是這樣的,發布了一個經過異步化改造的ASP.NET程序,其中有這樣一個同步方法:
public static void Notify(string title, string content, int recipientId) { //... }
被改造為異步方法:
public static async Task Notify(string title, string content, int recipientId) { //await ... }
之前在WebForms(.aspx)中是這樣同步調用它的:
<script runat="server"> void Page_Load(Object sender, EventArgs e) { //... MsgService.Notify(title, body, userId); //... } </script>
現在改為在MVC Controller Action中異步調用它:
public class ApplyController : Controller { [HttpPost] public async Task<string> Pass() { //... await MsgService.Notify(title, body, userId); //... } }
這次發布就是為了用MVC取代WebForms,但發布時同步調用Notify()方法的.aspx文件沒有從服務器上刪除。
發布后,這個ASP.NET程序跑一會就崩潰(crash),具體表現為:
a)訪問網站出現503錯誤;
b)IIS管理器中顯示對應的應用程序池處于停止狀態;
c)在Windows事件日志中發現以下三個錯誤:
日志1:
發生了未經處理的異常,已終止進程。 Application ID: /LM/W3SVC/15/ROOT Process ID: 23808 Exception: System.NullReferenceException Message: 未將對象引用設置到對象的實例。 StackTrace:
在 System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext) 在 System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext) 在 System.Web.LegacyAspNetSynchronizationContext.CallCallbackPossiblyUnderLock(SendOrPostCallback callback, Object state) 在 System.Web.LegacyAspNetSynchronizationContext.CallCallback(SendOrPostCallback callback, Object state) 在 System.Threading.Tasks.AwaitTaskContinuation.RunCallback(ContextCallback callback, Object state, Task& currentTask) --- 引發異常的上一位置中堆棧跟蹤的末尾 --- 在 System.Threading.Tasks.AwaitTaskContinuation.<ThrowAsyncIfNecessary>b__1(Object s) 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) 在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) 在 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() 在 System.Threading.ThreadPoolWorkQueue.Dispatch()
日志2:
應用程序: w3wp.exe Framework 版本: v4.0.30319 說明: 由于未經處理的異常,進程終止。 異常信息: System.NullReferenceException 堆棧: 在 System.Threading.Tasks.AwaitTaskContinuation.<ThrowAsyncIfNecessary>b__1(System.Object) 在 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) 在 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) 在 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() 在 System.Threading.ThreadPoolWorkQueue.Dispatch()
日志3:
Faulting application name: w3wp.exe, version: 7.5.7601.17514, time stamp: 0x4ce7afa2 Faulting module name: KERNELBASE.dll, version: 6.1.7601.18798, time stamp: 0x5507b87a Exception code: 0xe0434352 Fault offset: 0x000000000001aaad Faulting process id: 0x5d00 Faulting application start time: 0x01d0b86f3af9058e Faulting application path: c:\windows\system32\inetsrv\w3wp.exe Faulting module path: C:\Windows\system32\KERNELBASE.dll Report Id: 7bec0e6c-2462-11e5-b24e-c43d8baaa802
從日志信息看,問題肯定是異步引起的,于是檢查所有進行異步調用的代碼,沒發現問題(唯獨沒有檢查那個以為不在使用、沒有刪除的.aspx文件)。
后來才想到那個沒有刪除的.aspx文件,可是它已經被MVC取代了,沒在使用啊。如果是它引起的,只有一個可能。。。這個文件依然在被某些請求訪問。仔細排查后發現原來是引用js的地方沒加hash參數,造成有些客戶端瀏覽器由于緩存的原因還在使用舊版的js,舊版的js還會向這個.aspx文件發出ajax請求。
原來是一個疏忽造成了在同步方法中直接調用異步方法,但怎么也沒想到竟然有如此大的威力,能引起整個應用程序的崩潰,于是好奇心被激發。
看了網上的一些資料后,對這個問題有了一些認識。
在ASP.NET中(ASP.NET天生是多線程的,基于線程池的,沒有UI線程的概念),如果你調用了一個async方法,如果有await相伴,當前線程立馬被釋放回線程池,線程的上下文信息(比如reqeust context)被保存;如果沒有await相伴(也沒有其他的wait代碼),調用async方法之后,代碼會繼續往下執行,直至完成,當前線程被釋放回線程池,線程的上下文信息不會被保存。當async中的異步任務完成后(注:異步任務不是在另外一個線程中完成的,是在一個狀態機中完成的),會從線程池中取出一個線程繼續執行,執行時會讀取當時調用它的原線程的上下文信息(默認情況下的行為,如果ConfigureAwait(false) ,就沒有這一步操作),如果當初調用時沒有使用await,線程的上下文信息沒有被保存,這時就會引發NullReferenceException。而在這種級別發生的未處理null引用異常,會引發整個應用程序崩潰,更準確地說是應用程序所在的進程崩潰。因為這樣的異常實在太危險,為了不讓一只老鼠壞了一鍋湯,只能被犧牲。
所以,如果不想被犧牲,要么老老實實地await;要么告訴async方法,不要讀取原線程的上下文信息(ConfigureAwait(false),未經實際驗證是否有效);要么調用async方法的線程沒有需要保存的上下文信息,比如在Task.Run(或Task.Factory.StartNew)中調用async方法,也就是用一個新的線程調用async方法。
【推薦閱讀】
Best practice to call ConfigureAwait for all server-side code
Difference between the TPL & async/await (Thread handling)
Does an async void method create a new thread everytime it is called?
文章列表