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