• 水火难容:同步方法调用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:windowssystem32inetsrvw3wp.exe
    Faulting module path: C:Windowssystem32KERNELBASE.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? 

  • 相关阅读:
    判断微信浏览器
    文章迁移
    ECharts使用—折线图动态加载
    vue-cli入门
    gulp使用详解
    gulp使用入门
    Chrome扩展插件流程
    div界面元素生成图片
    xss攻击与防御
    bootstrap-table使用详解
  • 原文地址:https://www.cnblogs.com/dudu/p/crash-when-sync-call-async.html
Copyright © 2020-2023  润新知