ASP.NET应用同步调用async方法崩溃解决

作者:袖梨 2022-06-25

之前只知道在同步方法中调用异步(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)中是这样同步调用它的:



现在改为在MVC Controller Action中异步调用它:


public class ApplyController : Controller
{
    [HttpPost]
    public async Task 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.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.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方法。




C#的async和await

async/task/await三组合是.NET Framework 4.5带给.NET开发者的大礼,合理地使用它,可以提高应用程序的吞吐能力。

但是如果不正确使用,会带来意想不到的问题――比如await之后一直在等待。

先看一段ASP.NET MVC示例代码:


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

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

    private async Task 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,但由于在方法体中没有使用await,所以实际还是以同步的方式执行的,与直接使用ActionResult是一样的。
    GetResponseHtml是同步方法,GetResponseContentAsync是异步方法,在GetResponseHtml中调用了异步的GetResponseContentAsync。(如果调用的是第三方程序集,我们就不知道在GetResponseHtml中进行了异步调用,所以这个方法的设计是有问题的)

这段代码执行结果会是怎样呢?

――结果就是没有结果,一直在执行。。。

(注:如果在控制台应用程序中调用同样的GetResponseHtml,不会出现这个问题)

那如果解决这个问题呢:

解决方法一:在MVC Action中开启一个Task进行await

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


解决方法二:将GetResponseHtml变成异步方法

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

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


显然,第2个解决方法是更好的。

所以,我们在设计一个方法(method)时,如果调用了async方法,一定要将这个方法本身设计为async的。不然,别人调用时就一直等。

相关文章

精彩推荐