【问题标题】:Write in ASP.NET response output stream from task continuation从任务继续写入 ASP.NET 响应输出流
【发布时间】:2014-07-01 12:35:19
【问题描述】:

我有一个 http 处理程序,它应该写入输出一些文本。文本内容是异步检索的,所以我想在 ProcessRequest 方法中写入响应流,如下所示:

GetContent().ContinueWith(task => 
{
    using (var stream = task.Result)
    {
        stream.WriteTo(context.Response.OutputStream);
    }
});

但是,我得到一个带有堆栈跟踪的 NullReferenceException

in System.Web.HttpWriter.BufferData(Byte[] data, Int32 offset, Int32 size, Boolean needToCopyData)
   in System.Web.HttpWriter.WriteFromStream(Byte[] data, Int32 offset, Int32 size)
   in System.Web.HttpResponseStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   in System.IO.MemoryStream.WriteTo(Stream stream)
   in SomeHandler.<>c__DisplayClass1.<ProcessRequest>b__0(Task`1 task) in SomeHandler.cs:line 33
   in System.Threading.Tasks.ContinuationTaskFromResultTask`1.InnerInvoke()
   in System.Threading.Tasks.Task.Execute()

如果我不使用 ContinueWith 并在 task.Wait() 之后写入响应 - 没有错误,但显然这不是解决方案。

var task = GetContent();
task.Wait();
using (var stream = task.Result)
{
    stream.WriteTo(context.Response.OutputStream);
}

如何消除此错误? (使用.net 4.0)

【问题讨论】:

  • 你能使用现代风格的c#吗?你能用async/await 模式做吗?
  • 在调用ContinueWith 时,响应可能已经发送。
  • 哦,忘了说 - 我用的是 .net 4.0
  • 那么回答我的问题为“否”。我已标记您的问题以表示这一点。
  • @Noseratio,我正在使用 VS 2013

标签: c# asp.net asynchronous .net-4.0 task-parallel-library


【解决方案1】:

你需要实现IHttpAsyncHandler。检查"Walkthrough: Creating an Asynchronous HTTP Handler"

除此之外,您还可以使用async/await 复制流(请注意下面的CopyAsync)。为了能够使用 async/await 并将 .NET 4.0 与 VS2012+ 结合使用,请将 Microsoft.Bcl.Async 包添加到您的项目中。

这样,没有线程是不必要的阻塞。一个完整的例子(未经测试):

public partial class AsyncHandler : IHttpAsyncHandler
{
    async Task CopyAsync(HttpContext context)
    {
        using (var stream = await GetContentAsync(context))
        {
            await stream.CopyToAsync(context.Response.OutputStream);
        }
    }

    #region IHttpAsyncHandler
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        return new AsyncResult(cb, extraData, CopyAsync(context));
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        // at this point, the task has compeleted
        // we use Wait() only to re-throw any errors
        ((AsyncResult)result).Task.Wait();
    }

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        throw new NotImplementedException();
    }
    #endregion

    #region AsyncResult
    class AsyncResult : IAsyncResult
    {
        object _state;
        Task _task;
        bool _completedSynchronously;

        public AsyncResult(AsyncCallback callback, object state, Task task)
        {
            _state = state;
            _task = task;
            _completedSynchronously = _task.IsCompleted;
            _task.ContinueWith(t => callback(this), TaskContinuationOptions.ExecuteSynchronously);
        }

        public Task Task
        {
            get { return _task; }
        }

        #region IAsyncResult
        public object AsyncState
        {
            get { return _state; }
        }

        public System.Threading.WaitHandle AsyncWaitHandle
        {
            get { return ((IAsyncResult)_task).AsyncWaitHandle; }
        }

        public bool CompletedSynchronously
        {
            get { return _completedSynchronously; }
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }
        #endregion
    }
    #endregion
}

【讨论】:

  • 谢谢。这正是我所需要的。你能描述一下 - 为什么我们需要在 EndProcessRequest 等待任务?
  • @eternity,Wait() 用于观察任何可能由Task 逻辑引发的异常。如果任务成功完成,Wait 什么都不做,响应成功发送到客户端。否则,将重新引发异常并将其传播到 ASP.NET。请注意,EndProcessRequest 是从 cb 回调内部调用的,它由 ASP.NET 提供,我们从 ContinueWith 调用。
【解决方案2】:

这个问题可能是基于任务在自己的线程中执行时发生的“上下文切换”。

当前HttpContext 仅在请求线程中可用。但是,您可以创建一个“本地引用”以便能够访问它(但这并不能完全解决您的问题 - 见下文):

var outputStream = context.Response.OutputStream;
GetContent().ContinueWith(task => 
{
    using (var stream = task.Result)
    {
        stream.WriteTo(outputStream);
    }
});

现在的问题是,当 ContinueWith 执行时,您的请求很可能已经完成,因此您的客户端流已经关闭。
在此之前,您需要将内容写入流。

我需要删除我的答案的以下部分。 Http 处理程序在 4.0 中不支持 async,因为这需要在 4.5 之前不可用的 HttpTaskAsyncHandler 基类 => 我不知道在 4.0 中的 ProcessRequest 方法中绕过 Wait 的方法时使用 http 处理程序:

我建议使用类似以下的内容:

using(var result = await GetContent())
{
    stream.WriteTo(context.Response.OutputStream);
}

和相应的NuGet-Package for await

【讨论】:

  • 不,错误发生在 MemoryStream.WriteTo 中。这意味着上下文不为空。
  • @usr: 是的,那么也许是因为我的第二点:流到达BufferData...时已经关闭了...
  • 这不会导致 BCL 内部崩溃。这会导致一些适当的异常。
  • ...为什么NullReferenceException 不是“适当的例外”?
  • 因为它表明发生了一个错误,并不能帮助调用者解决问题。 BCL 中的所有类都设计为不会因 nullref、索引超出范围等内容而崩溃……它们会因描述性消息而崩溃。
【解决方案3】:

最近,我遇到了类似的任务来编写一个 ashx 处理程序来异步放置响应。目的是生成一些大字符串,执行 I/O 并将其返回到响应流中,从而对 ASP.NET 与其他 languages like node.js 进行基准测试,以用于我将要开发的大量 I/O 绑定应用程序。这就是我最终要做的(它有效):

    private StringBuilder payload = null;

    private async void processAsync()
    {
        var r = new Random (DateTime.Now.Ticks.GetHashCode());

        //generate a random string of 108kb
        payload=new StringBuilder();
        for (var i = 0; i < 54000; i++)
            payload.Append( (char)(r.Next(65,90)));

        //create a unique file
        var fname = "";
        do
        {
            //fname = @"c:\source\csharp\asyncdemo\" + r.Next (1, 99999999).ToString () + ".txt";
            fname =  r.Next (1, 99999999).ToString () + ".txt";
        } while(File.Exists(fname));            

        //write the string to disk in async manner
        using(FileStream fs = new FileStream(fname,FileMode.CreateNew,FileAccess.Write, FileShare.None,
            bufferSize: 4096, useAsync: true))
        {
            var bytes=(new System.Text.ASCIIEncoding ()).GetBytes (payload.ToString());
            await fs.WriteAsync (bytes,0,bytes.Length);
            fs.Close ();
        }

        //read the string back from disk in async manner
        payload = new StringBuilder ();
        //FileStream ;
        //payload.Append(await fs.ReadToEndAsync ());
        using (var fs = new FileStream (fname, FileMode.Open, FileAccess.Read,
                   FileShare.Read, bufferSize: 4096, useAsync: true)) {
            int numRead;
            byte[] buffer = new byte[0x1000];
            while ((numRead = await fs.ReadAsync (buffer, 0, buffer.Length)) != 0) {
                payload.Append (Encoding.Unicode.GetString (buffer, 0, numRead));
            }
        }


        //fs.Close ();
        //File.Delete (fname); //remove the file
    }

    public void ProcessRequest (HttpContext context)
    {
        Task task = new Task(processAsync);
        task.Start ();
        task.Wait ();

        //write the string back on the response stream
        context.Response.ContentType = "text/plain";
        context.Response.Write (payload.ToString());
    }

【讨论】:

  • 这不是真正的异步,因为请求线程在 task.Wait() 处被阻塞 - 要“真正异步”,该线程应该在等待期间释放(可以通过使用 await 来实现)。
  • 除了@chrfin 的观点,将async void 方法传递给new Task(processAsync) 并不像您想象的那样工作。一旦到达processAsync 内的第一个await,此任务就完成了。
  • @chrfin - 原始问题还包含使用 Wait() 方法的相同逻辑。而且 Wait() 实际上并没有阻止较低级别的请求。阅读这篇 MSDN 文章:blogs.msdn.com/b/pfxteam/archive/2009/10/15/9907713.aspx
  • And Wait() doesn't actually block requests on a lower level - 是的。再次阅读您的链接文章!它可能不使用一个额外的线程池线程,但它肯定会阻塞请求线程,因此 IIS 无法使用它执行另一个请求 - 在这种情况下,这就是“异步”的重点,因为这些请求线程是有限的。 @down-votes:你得到了两个反对票和两个解释,那你还想要什么?
  • 再次:当Wait 运行时,IIS 不能使用线程,这就是你应该避免的。您应该使用await task;,因为这确实会将线程释放回 IIS,直到 IO 完成并随后返回。
猜你喜欢
  • 2018-07-08
  • 2019-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多