水火难容:同步方法调用async方法引发的ASP.NET应用程序崩溃

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

时间: 2024-08-03 08:57:26

水火难容:同步方法调用async方法引发的ASP.NET应用程序崩溃的相关文章

VS2010 F5调试时出现:“ 尝试运行项目时出错:未捕获通过反射调用的方法引发的异常”解决

S2010 F5调试时出现 尝试运行项目时出错:未捕获通过反射调用的方法引发的异常 两个解决方法:1) 打开项目属性,选择调试选项卡,将“启用非托管代码调试”一项钩上.2) 打开项目属性,选择调试选项卡,将“启用Visual Studio宿主进程“一项钩掉. 原文地址:https://www.cnblogs.com/gisoracle/p/9690123.html

ASP.NET MVC 如何在一个同步方法(非async)方法中等待async方法

问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public ActionResult Asv2() { //dead lock var task = AssignValue2(); task.Wait(); return Content(_container); } private void Assign() { _container = "Hello World"; } public async Task AssignVa

MVC 如何在一个同步方法(非async)方法中等待async方法

MVC 如何在一个同步方法(非async)方法中等待async方法 问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public ActionResult Asv2() { //dead lock var task = AssignValue2(); task.Wait(); return Content(_container); } private void Assign() { _container = "Hello World&q

【转载】C++程序崩溃排查方法

windows下C++程序release版本崩溃错误排查方法. 一个你精心设计的24小时不间断运行,多线程的程序,突然运行了几个月后崩了,此问题是非常难以排查的,也是很头疼的问题. 现利用Google开源工具crashrpt与Microsoft windbg工具,解决这个问题,并分享给大家. 使用工具Crashrpt.Windbg.因为windbg这个工具很常见,暂不介绍.其中重点介绍一下crashrpt. 一.crashrpt 简介 crashrpt是一个包含能够在程序出现各种类型未处理异常时

Asp.net中ajax调用后台方法

这个当然不用说大家都知道的一种就是ajax调后台的方法.1.有参数的方法调用示例代码如下:前台jQuery代码:$(function() {  这个当然不用说大家都知道的一种就是ajax调后台的方法. 1.有参数的方法调用 示例代码如下: 前台jQuery代码: $(function() {   var browers = browersEstimate();   var params = '{browersType:"' + browers + '"}';   $.ajax({   

ASP.NET jQuery 事件里调用后台方法

利用js 调用后台写的方法 1 <script src="js/jquery-1.7.1.min.js"></script> 2 2 <script> 3 3 $(function () { 4 4 $(".btn").click(function () { 5 5 $("#txtname").val('<%=Session["Name"]%>'); 6 6 $("#t

Asp.net中JQuery、ajax调用后台方法总结

通过上一篇文章实例的实现,整个过程当中学习到很多知识点,了解了Jquery.Ajax在asp.net中的运用,加以总结,其实原理都是一样的,理解了一种,其他的注意很少的区别就可以了.灵活运用: 1.有参数的方法调用 示例代码如下:前台jQuery代码:$(function() {  知道的一种就是ajax调后台的方法. 1.有参数的方法调用 示例代码如下: 前台jQuery代码: [plain] view plaincopyprint? <span style="font-size:18p

ASP.NET MVC AJAX调用JsonResult方法并返回自定义错误信息

一.如何用AJAX调用JsonResult方法 比如FuckController中添加有个返回JsonResult类型的方法FuckJson(): public JsonResult FuckJson() { return new JsonResult() { Data = new List<string>() { "fuck", "shit" }, JsonRequestBehavior = JsonRequestBehavior.AllowGet }

asp.net javascript客户端调用服务器端方法

如何用js调用服务器端方法.首先服务器端方法的格式如下 [System.Web.Services.WebMethod]        public static void serverMethod(string url)        {            WebClient wc = new WebClient();            StringBuilder postData = new StringBuilder();            postData.Append("mu