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

之前只知道在同步方法中调用异步(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的。不然,别人调用时就一直等。

时间: 2024-10-28 10:09:43

ASP.NET应用同步调用async方法崩溃解决的相关文章

Android 调用notifyDataSetChanged方法失败解决办法

Android 调用notifyDataSetChanged方法失败解决办法 如果使用ListView.GridView等进行数据展示,当绑定的数据有了更新的时候,需要实时刷新ListView,即调用Adapter的notifyDataSetChanged方法,但是很多人在调用之后会发现ListView是数据并没有刷新,这是什么原因呢?下面将对其进行详细解释. 1.数据源没有更新,调用notifyDataSetChanged无效. 2.数据源更新了,但是它指向新的引用,调用notifyDataS

ExtJS调用focus方法无效解决办法

在BoxComponent文档中对focus方法的说明如下:  代码如下 复制代码 focus( [Boolean selectText], [Boolean/Number delay] ) : Ext.Component Try to focus this component. 可以看到这个方法有两个可选的参数,第一个的含义是是否全选中所在的文本内容(默认为false),第二个是设置一个延迟的时间,单位是毫秒(默认为0,不延迟).下面是两种调用的方法:  代码如下 复制代码  var text

Ext.net如何同步调用后台方法?版本:1.2

问题描述 需求:在用户点击浏览器关闭按钮时执行自己的业务逻辑.在业务逻辑成功执行完以后再往下执行代码:return"RunOnBeforeUnload";functionRunOnBeforeUnload(){Ext.net.DirectMethods.onunload({async:false,success:function(){alert("注销成功");}})return"RunOnBeforeUnload";} 默认Ext.net.Di

remote script文档(转载自微软)&amp;lt;五&amp;gt;同步调用 Remote Scripti

文档: 调用 Remote Scripting 方法同步在对某个客户页和某个服务器页上的 remote scripting 进行配置后,您就可以从自己的客户脚本调用该服务器页的方法了.缺省的情况是,当用户调用某个服务器方法时,它被同步执行──您的客户脚本直到服务器方法执行完毕并返回结果后才能停止运行.一般说来,当您在自己的客户脚本中需要服务器方法结果时,则需同步调用服务器方法.注意 您也可以异步调用服务器方法.有关详细信息,请参阅异步调用 Remote Scripting 方法.当您调用某个服务

Spring源码学习之:@async 方法上添加该注解实现异步调用的原理

在我们使用spring框架的过程中,在很多时候我们会使用@async注解来异步执行某一些方法,提高系统的执行效率.今天我们来探讨下 spring 是如何完成这个功能的.     spring 在扫描bean的时候会扫描方法上是否包含@async的注解,如果包含的,spring会为这个bean动态的生成一个子类,我们称之为代理类(?), 代理类是继承我们所写的bean的,然后把代理类注入进来,那此时,在执行此方法的时候,会到代理类中,代理类判断了此方法需要异步执行,就不会调用父类 (我们原本写的b

在ASP.NET中调用存储过程方法新解

asp.net|存储过程 在使用.NET的过程中,数据库访问是一个很重要的部分,特别是在B/S系统的构建过程中,数据库操作几乎成为了一个必不可少的操作.调用存储过程实现数据库操作使很多程序员使用的方法,而且大多数的程序员都是能使用存储过程就使用存储过程,很少直接使用SQL语句,所以存储过程是很有用而且很重要的. 存储过程简介 简单的说,存储过程是由一些SQL语句和控制语句组成的被封装起来的过程,它驻留在数据库中,可以被客户应用程序调用,也可以从另一个过程或触发器调用.它的参数可以被传递和返回.与

asp.net调用存储过程方法新解

asp.net|存储过程 在使用.net的过程中,数据库访问是一个很重要的部分,特别是在b/s系统的构建过程中,数据库操作几乎成为了一个必不可少的操作.调用存储过程实现数据库操作使很多程序员使用的方法,而且大多数的程序员都是能使用存储过程就使用存储过程,很少直接使用sql语句,所以存储过程是很有用而且很重要的. 存储过程简介 简单的说,存储过程是由一些sql语句和控制语句组成的被封装起来的过程,它驻留在数据库中,可以被客户应用程序调用,也可以从另一个过程或触发器调用.它的参数可以被传递和返回.与

asp.net在后端动态添加样式表调用的方法

  asp.net在后端动态添加样式表调用的方法         本篇文章主要介绍了asp.net在后端动态添加样式表调用的方法,涉及asp.net操作样式表的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下 本文实例讲述了asp.net在后端动态添加样式表调用的方法.分享给大家供大家参考.具体实现方法如下: ? 1 2 3 4 5 HtmlLink CssControl = new HtmlLink(); CssControl.Href = url; CssControl.Attribut

asp.net-asp. net 前台调用后台的一个方法,点击按扭时如何调用呢,调用如下方法

问题描述 asp. net 前台调用后台的一个方法,点击按扭时如何调用呢,调用如下方法 void test() { char[] constant = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; StringBuilder ascii = new StringBuilder(); Random random = new Random(); for (int i = 0; i < 6; i++) { ascii.Append(cons