文章出處

之前只知道在同步方法中調用異步(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? 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()