由ASP.NET网页请求触发的异步操作运行

60

我有一个异步操作,由于多种原因需要使用ASP.NET web页面的HTTP调用来触发。当请求我的页面时,它应该启动此操作并立即向客户端返回确认。

这个方法也通过WCF Web服务公开,而且它运行得非常完美。

在我第一次尝试时,抛出了一个异常,告诉我:

在这个上下文中不允许异步操作。
启动异步操作的页面必须设置Async属性为true,
异步操作只能在PreRenderComplete事件之前的页面上启动。

所以我当然在@Page指令中添加了Async="true"参数。现在,我没有收到错误消息,但是页面会阻塞,直到异步操作完成。

如何才能实现真正的“点火并忘记”页面?

编辑:一些代码提供更多信息。它比这要复杂一些,但我已经尽量在其中表达了基本思想。

public partial class SendMessagePage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string message = Request.QueryString["Message"];
        string clientId = Request.QueryString["ClientId"];

        AsyncMessageSender sender = new AsyncMessageSender(clientId, message);
        sender.Start();

        Response.Write("Success");
    }
}

AsyncMessageSender类:

public class AsyncMessageSender
{
    private BackgroundWorker backgroundWorker;
    private string client;
    private string msg;

    public AsyncMessageSender(string clientId, string message)
    {
        this.client = clientId;
        this.msg = message;

        // setup background thread to listen
        backgroundThread = new BackgroundWorker();
        backgroundThread.WorkerSupportsCancellation = true;
        backgroundThread.DoWork += new DoWorkEventHandler(backgroundThread_DoWork);
    }

    public void Start()
    {
        backgroundThread.RunWorkerAsync();
    }

    ...
    // after that it's pretty predictable
}

很难回答,如果没有看到调用此异步操作的代码。 - Bryan
在Azure上,您可以使用Web Jobs。请参见http://curah.microsoft.com/52143/using-the-webjobs-feature-of-windows-azure-web-sites。在ASP.NET上的后台线程存在问题。 - RickAndMSFT
Web工作不免费?@RickAndMSFT - Kiquenet
5个回答

45

如果您正在运行WebForms,请在发出请求的.aspx页面中设置Ansync ="true"。
<%@ Page Language="C#" Async="true" ... %>


32

如果你不关心向用户返回任何内容,你可以启动一个单独的线程,或者采用快速而简便的方法,使用委托并异步调用它。 如果你不关心异步任务完成时通知用户,可以忽略回调函数。 尝试在SomeVeryLongAction()方法的末尾设置断点,你会发现它在页面已经被提供后才运行完。

private delegate void DoStuff(); //delegate for the action

protected void Page_Load(object sender, EventArgs e)
{

}

protected void Button1_Click(object sender, EventArgs e)
{
    //create the delegate
    DoStuff myAction = new DoStuff(SomeVeryLongAction); 
    //invoke it asynchrnously, control passes to next statement
    myAction.BeginInvoke(null, null);
    Button1.Text = DateTime.Now.ToString();
}


private void SomeVeryLongAction()
{
    for (int i = 0; i < 100; i++)
    {
        //simulation of some VERY long job
        System.Threading.Thread.Sleep(100);
    }
}

谢谢,BeginInvoke 是关键 - 我不确定为什么我没有尝试过那个! - Damovisa
3
危险!忽略回调将导致资源未被清理。这是我建议采用不同方法的一个原因。Jeffrey Richter在他的CLR via C#书中讨论了这个问题(标题可能不完全正确)。 - Charlie Flowers
3
必须调用EndInvoke来确保清理。非常正确。好的讨论: http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/d88d3f1e-f4aa-40c1-b1b6-e79e801f3909/http://msdn.microsoft.com/en-us/magazine/cc164036(printer).aspx - Ilya Tchivilev
2
不要使用beginInvoke/endInvoke来进行“fire-and-forget”操作,应该使用ThreadPool.QueueUserWorkItem(o => FireAway())。这个建议来源于https://dev59.com/z3NA5IYBdhLWcg3wUL1Y#1018630 和 http://haacked.com/archive/2009/01/09/asynchronous-fire-and-forget-with-lambdas.aspx。 - Michael Freidgeim

27

好的,这里是问题:Async属性用于当您的页面将调用一些长时间运行并且还会阻塞线程的任务,然后您的页面需要该任务的输出以便将信息返回给用户的情况。例如,如果您的页面需要调用Web服务,等待其响应,然后使用响应中的数据来呈现页面。

使用Async属性的原因是避免阻塞线程。这很重要,因为ASP.NET应用程序使用线程池来处理请求,并且只有相对较少的线程可用。如果每个调用在等待Web服务调用时都占用线程,那么很快你将达到足够的并发用户数,用户将不得不等待这些Web服务调用完成。Async属性使线程返回到线程池并为您的网站的其他并发访问者提供服务,而不是强制其静止不动地等待Web服务调用返回。

对您来说,重要的是:异步任务完成之前无法呈现页面,Async属性就是为此设计的,这就是为什么它不会立即呈现页面的原因。

您需要启动自己的线程,并将其设置为守护线程。我不记得确切的语法,但您可以通过在BCL文档中搜索“daemon”找到它。这意味着该线程将保持活动状态,从而防止应用程序关闭,这很重要,因为ASP.NET和IIS保留在必要时“回收您的进程”的权利,如果发生这种情况,而您的线程正在工作,则任务将停止。使线程成为守护线程将防止这种情况发生(除了可能的一些稀有边界情况...当您找到此文档的文档时,您将了解更多信息)。

那个守护线程是您将启动这些任务的位置。并且在告诉守护线程执行任务之后,您可以立即呈现页面...因此页面的呈现会立即发生。

比ASP.NET进程中的守护程序线程更好的选择是为执行任务实现一个Windows服务。让ASP.NET应用程序将要执行的任务通信给服务,无需守护程序线程,并且不必担心ASP.NET进程被回收。如何告诉服务执行任务呢?可以通过WCF(Windows Communication Foundation),也可以通过向服务轮询的数据库表中插入记录等多种方式。
编辑:这里有另一个想法,我之前为此目的使用过。将关于任务的信息写入MSMQ队列中,然后再由另一个进程(甚至在另一台机器上)从队列中取出并执行耗时的任务。将任务插入队列的工作被优化为尽快返回,因此当你放入队列中的数据被传输或其他类似情况时,线程不会被阻塞。这是最快的方法之一,可以在不等待任务执行的情况下记下需要执行任务的事实。

非常好的答案 - 感谢您的回答。我稍后实际上会使用MSMQ。我真正需要做的就是像您建议的那样启动另一个线程来完成我的工作。正如您所提到的,@Page指令中的Async参数对我没有帮助。 - Damovisa
好的,很高兴能帮到你。顺便说一下,请务必查看我在另一个答案中的评论...忽略EndInvoke方面会导致资源泄漏!而确保您可以处理EndInvoke的唯一可靠方法是拥有守护线程。所以你要回到那个或者使用MSMQ / Service。 - Charlie Flowers
谢谢你 - 我实际上重用了一些做类似事情的代码,只是在EndInvoke上没有做任何事情。 - Damovisa

3
您可以很容易地避开这个限制,甚至不需要将Async设置为true。
public void Start()
{
    new Task(() =>
    {
        backgroundThread.RunWorkerAsync();
    }).Start();
}

1
如果在异步调用 Web 服务时出现此错误,请确保按照异常消息的指示添加 Async='true' 属性?
在页面顶部添加以下代码:< Page Language='VB' Async='true' AutoEventWireup='false' CodeFile='mynewpage.aspx.vb' Inherits='mynewpage' %>.

3
这个问题的答案在3年前被接受。原始问题中还提到了Async = true。欢迎来到SO。 - Michael Freidgeim
1
@nathanchere 如果你仔细看,我在2012年已经回答了这个问题。你有机会给那个在2013年抄袭我的答案并被接受了14次的人投反对票吗? - Amrik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接