ASP.NET應用同步調用async方法崩潰解決

來源:互聯網
上載者:User

之前只知道在同步方法中調用非同步(async)方法時,如果用.Result等待調用結果,會造成線程死結(deadlock)。

昨天一個偶然的情況,造成在同步方法中調用了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方法。




C#的async和await

async/task/await三組合是.NET Framework 4.5帶給.NET開發人員的大禮,合理地使用它,可以提高應用程式的吞吐能力。

但是如果不正確使用,會帶來意想不到的問題——比如await之後一直在等待。

先看一段ASP.NET MVC範例程式碼:


public class BlogController : Controller
{
    public async Task<ActionResult> AwaitDemo()
    {
        var responseHtml = GetResponseHtml("http://www.cnblogs.com/");
        return Content(responseHtml);
    }

    private string GetResponseHtml(string url)
    {
        return GetResponseContentAsync(url).Result;
    }

    private async Task<string> GetResponseContentAsync(string url)
    {
        var httpClient = new System.Net.Http.HttpClient();
        var response = await httpClient.GetAsync(url);
        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            return await response.Content.ReadAsStringAsync();
        }
        else
        {
            return "error";
        }
    }
}

代碼說明:

    在上面的代碼中,雖然在Action方法之前加了async Task<ActionResult>,但由於在方法體中沒有使用await,所以實際還是以同步的方式執行的,與直接使用ActionResult是一樣的。
    GetResponseHtml是同步方法,GetResponseContentAsync是非同步方法呼叫,在GetResponseHtml中調用了非同步GetResponseContentAsync。(如果調用的是第三方程式集,我們就不知道在GetResponseHtml中進行了非同步呼叫,所以這個方法的設計是有問題的)

這段代碼執行結果會是怎樣呢?

——結果就是沒有結果,一直在執行。。。

(註:如果在控制台應用程式中調用同樣的GetResponseHtml,不會出現這個問題)

那如果解決這個問題呢:

解決方案一:在MVC Action中開啟一個Task進行await

public async Task<ActionResult> AwaitDemo()
{
    var responseHtml = await Task.Factory.StartNew(() =>
        GetResponseHtml("http://www.cnblogs.com/"));
    return Content(responseHtml);
}


解決方案二:將GetResponseHtml變成非同步方法呼叫

public async Task<ActionResult> AwaitDemo()
{
    var responseHtml = await GetResponseHtml("http://www.cnblogs.com/");
    return Content(responseHtml);
}

private async Task<string> GetResponseHtml(string url)
{
    return await GetResponseContentAsync(url);
}


顯然,第2個解決方案是更好的。

所以,我們在設計一個方法(method)時,如果調用了async方法,一定要將這個方法本身設計為async的。不然,別人調用時就一直等。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.