从任务继续中编写ASP.NET响应输出流

4

我有一个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模式下完成吗? - Daniel A. White
当调用ContinueWith时,响应很可能已经被发送。 - Daniel A. White
哦,忘了提一下 - 我使用的是 .net 4.0。 - eternity
有一个 NuGet 包可用于在 4.0 中使用 async/await:http://www.nuget.org/packages/Microsoft.Bcl.Async/ - Christoph Fink
1
@Noseratio,我正在使用VS 2013。 - eternity
显示剩余3条评论
3个回答

4
您需要实现接口。请查看“创建异步HTTP处理程序的步骤”。
除此之外,您可以使用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
}

1
谢谢。这正是我需要的。你能描述一下为什么我们需要在EndProcessRequest中等待任务吗? - eternity
1
@eternity,Wait() 的作用是观察 Task 逻辑可能抛出的任何异常。如果任务成功完成,则 Wait 不执行任何操作,并将响应成功发送给客户端。否则,异常会被重新抛出并传播到 ASP.NET。请注意,EndProcessRequest 是从 cb 回调函数内部调用的,该回调函数由 ASP.NET 提供,并且我们从 ContinueWith 中调用它。 - noseratio - open to work

0
这个问题可能基于“上下文切换”,当任务在其自己的线程中执行时会发生。
当前的HttpContext仅在请求线程中可用。但是,您可以创建一个“本地引用”以便访问它(但这并不能完全解决您的问题-请参见下文):
var outputStream = context.Response.OutputStream;
GetContent().ContinueWith(task => 
{
    using (var stream = task.Result)
    {
        stream.WriteTo(outputStream);
    }
});

现在的问题是,当执行ContinueWith时,您的请求很可能已经完成,因此向客户端的流已经关闭。
您需要在此之前将内容写入流中。
我需要删除我的回答中的以下部分。在4.0中,Http处理程序不支持async,因为这需要HttpTaskAsyncHandler基类,而该基类在4.5之前不可用 => 当使用http处理程序时,在ProcessRequest方法中无法避免Wait我建议使用以下内容:
using(var result = await GetContent())
{
    stream.WriteTo(context.Response.OutputStream);
}

以及适用于 awaitNuGet-Package


不,错误发生在已经执行MemoryStream.WriteTo时。这意味着上下文不为null。 - usr
@usr:如果是真的,那可能是因为我的第二个观点:当流到达 BufferData 时,它已经关闭了... - Christoph Fink
这不会导致BCL内部崩溃,而是会引发一些适当的异常。 - usr
为什么NullReferenceException不算是一个“合适的异常”? - Christoph Fink
因为它表示出现了错误,而且不能帮助调用者解决问题。BCL 中的所有类都被设计成不会因为 nullref、索引越界等问题而崩溃……它们会输出描述性信息来崩溃。 - usr

-3
最近,我遇到了类似的任务,需要编写一个ashx处理程序来异步地放置响应。目的是生成一些大字符串,执行I/O并将其返回到响应流中,从而对ASP.NET进行基准测试,并与其他像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());
    }

1
这并不是真正的异步,因为请求线程在 task.Wait() 处被阻塞 - 要想实现真正的异步,该线程应在等待期间被释放(可以通过使用 await 实现)。 - Christoph Fink
@chrfin - 原始问题中也包含使用Wait()方法的相同逻辑。而Wait()实际上并不会在较低层次上阻止请求。请阅读此MSDN文章:http://blogs.msdn.com/b/pfxteam/archive/2009/10/15/9907713.aspx - Prahlad Yeri
有人可以解释一下为什么你要给我点踩吗? - Prahlad Yeri
3
“Wait()实际上并不会在较低级别上阻塞请求” - 实际上它会。请再次阅读您提供的链接文章!它可能不使用额外的线程池线程,但它肯定会阻塞请求线程,这样IIS就无法使用该线程执行另一个请求 - 这就是在这种情况下“异步”的要点,因为这些请求线程是有限的。 @down-votes:您已经获得了两个反对票和两个解释,那么您还想要什么? - Christoph Fink
1
再次强调:在Wait运行时,IIS无法使用该线程,这是应该避免的。您应该使用await task;,因为它会释放线程回到IIS,直到IO完成并在此之后返回。 - Christoph Fink
显示剩余3条评论

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