C# Web Api + HttpClient:异步模块或处理程序已完成,而异步操作仍待处理
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/15060214/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Web Api + HttpClient: An asynchronous module or handler completed while an asynchronous operation was still pending
提问by Gavin Osborn
I'm writing an application that proxies some HTTP requests using the ASP.NET Web API and I am struggling to identify the source of an intermittent error. It seems like a race condition... but I'm not entirely sure.
我正在编写一个使用 ASP.NET Web API 代理一些 HTTP 请求的应用程序,我正在努力确定间歇性错误的来源。这似乎是一种竞争条件……但我并不完全确定。
Before I go into detail here is the general communication flow of the application:
在我详细介绍之前,这里是应用程序的一般通信流程:
- Clientmakes a HTTP request to Proxy 1.
- Proxy 1relays the contents of the HTTP request to Proxy 2
- Proxy 2relays the contents of the HTTP request to the Target Web Application
- Target Web Appresponds to the HTTP request and the response is streamed (chunked transfer) to Proxy 2
- Proxy 2returns the response to Proxy 1which in turn responds to the original calling Client.
- 客户端向代理 1发出 HTTP 请求。
- 代理 1将 HTTP 请求的内容中继到代理 2
- 代理 2将 HTTP 请求的内容中继到目标 Web 应用程序
- 目标 Web 应用程序响应 HTTP 请求并将响应流式传输(分块传输)到代理 2
- 代理 2将响应返回给代理 1,代理 1又响应原始调用Client。
The Proxy applications are written in ASP.NET Web API RTM using .NET 4.5. The code to perform the relay looks like so:
代理应用程序是使用 .NET 4.5 在 ASP.NET Web API RTM 中编写的。执行中继的代码如下所示:
//Controller entry point.
public HttpResponseMessage Post()
{
using (var client = new HttpClient())
{
var request = BuildRelayHttpRequest(this.Request);
//HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
//As it begins to filter in.
var relayResult = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
var returnMessage = BuildResponse(relayResult);
return returnMessage;
}
}
private static HttpRequestMessage BuildRelayHttpRequest(HttpRequestMessage incomingRequest)
{
var requestUri = BuildRequestUri();
var relayRequest = new HttpRequestMessage(incomingRequest.Method, requestUri);
if (incomingRequest.Method != HttpMethod.Get && incomingRequest.Content != null)
{
relayRequest.Content = incomingRequest.Content;
}
//Copies all safe HTTP headers (mainly content) to the relay request
CopyHeaders(relayRequest, incomingRequest);
return relayRequest;
}
private static HttpRequestMessage BuildResponse(HttpResponseMessage responseMessage)
{
var returnMessage = Request.CreateResponse(responseMessage.StatusCode);
returnMessage.ReasonPhrase = responseMessage.ReasonPhrase;
returnMessage.Content = CopyContentStream(responseMessage);
//Copies all safe HTTP headers (mainly content) to the response
CopyHeaders(returnMessage, responseMessage);
}
private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
var content = new PushStreamContent(async (stream, context, transport) =>
await sourceContent.Content.ReadAsStreamAsync()
.ContinueWith(t1 => t1.Result.CopyToAsync(stream)
.ContinueWith(t2 => stream.Dispose())));
return content;
}
The error that occurs intermittently is:
间歇性出现的错误是:
An asynchronous module or handler completed while an asynchronous operation was still pending.
异步模块或处理程序已完成,而异步操作仍处于挂起状态。
This error usually occurs on the first few requests to the proxy applications after which the error is not seen again.
此错误通常发生在对代理应用程序的前几个请求中,之后就不会再次出现该错误。
Visual Studio never catches the Exception when thrown. But the error can be caught in the Global.asax Application_Error event. Unfortunately the Exception has no Stack Trace.
Visual Studio 永远不会在抛出异常时捕获异常。但是可以在 Global.asax Application_Error 事件中捕获错误。不幸的是,异常没有堆栈跟踪。
The proxy applications are hosted in Azure Web Roles.
代理应用程序托管在 Azure Web 角色中。
Any help identifying the culprit would be appreciated.
任何确定罪魁祸首的帮助将不胜感激。
采纳答案by Stephen Cleary
Your problem is a subtle one: the async
lambda you're passing to PushStreamContent
is being interpreted as an async void
(because the PushStreamContent
constructoronly takes Action
s as parameters). So there's a race condition between your module/handler completing and the completion of that async void
lambda.
您的问题是一个微妙的问题:async
您传递给的lambdaPushStreamContent
被解释为async void
( 因为PushStreamContent
构造函数只将Action
s 作为参数)。因此,您的模块/处理程序完成与该async void
lambda的完成之间存在竞争条件。
PostStreamContent
detects the stream closing and treats that as the end of its Task
(completing the module/handler), so you just need to be sure there's no async void
methods that could still run after the stream is closed. async Task
methods are OK, so this should fix it:
PostStreamContent
检测流关闭并将其视为其结束Task
(完成模块/处理程序),因此您只需要确保async void
在流关闭后没有仍然可以运行的方法。async Task
方法没问题,所以这应该解决它:
private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
Func<Stream, Task> copyStreamAsync = async stream =>
{
using (stream)
using (var sourceStream = await sourceContent.Content.ReadAsStreamAsync())
{
await sourceStream.CopyToAsync(stream);
}
};
var content = new PushStreamContent(stream => { var _ = copyStreamAsync(stream); });
return content;
}
If you want your proxies to scale a bit better, I also recommend getting rid of all the Result
calls:
如果你想让你的代理更好地扩展,我还建议你摆脱所有的Result
调用:
//Controller entry point.
public async Task<HttpResponseMessage> PostAsync()
{
using (var client = new HttpClient())
{
var request = BuildRelayHttpRequest(this.Request);
//HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
//As it begins to filter in.
var relayResult = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
var returnMessage = BuildResponse(relayResult);
return returnMessage;
}
}
Your former code would block one thread for each request (until the headers are received); by using async
all the way up to your controller level, you won't block a thread during that time.
您以前的代码会为每个请求阻塞一个线程(直到收到标头);通过一直使用async
到您的控制器级别,您将不会在此期间阻塞线程。
回答by Henrik Frystyk Nielsen
A slightly simpler model is that you can actually just use the HttpContents directly and pass them around inside the relay. I just uploaded a sample illustrating how you can rely both requests and responses asynchronously and without buffering the content in a relatively simple manner:
一个稍微简单的模型是您实际上可以直接使用 HttpContents 并将它们传递到中继内部。我刚刚上传了一个示例,说明如何以相对简单的方式异步依赖请求和响应,而不需要缓冲内容:
It is also beneficial to reuse the same HttpClient instance as this allows you to reuse connections where appropriate.
重用相同的 HttpClient 实例也是有益的,因为这允许您在适当的地方重用连接。
回答by Dave Parker
I would like to add some wisdom for anyone else who landed here with the same error, but all of your code seems fine. Look for any lambda expressions passed into functions across the call-tree from where this occurs.
我想为遇到相同错误的其他人添加一些智慧,但您的所有代码似乎都很好。在发生这种情况的整个调用树中查找传递给函数的任何 lambda 表达式。
I was getting this error on a JavaScript JSON call to an MVC 5.x controller action. Everything I was doing up and down the stack was defined async Task
and called using await
.
我在对 MVC 5.x 控制器操作的 JavaScript JSON 调用中收到此错误。我在堆栈中上下所做的一切都是async Task
使用await
.
However, using Visual Studio's "Set next statement" feature I systematically skipped over lines to determine which one caused it. I kept drilling down into local methods until I got to a call into an external NuGet package. The called method took an Action
as a parameter and the lambda expression passed in for this Action was preceded by the async
keyword. As Stephen Cleary points out above in his answer, this is treated as an async void
, which MVC does not like. Luckily said package had *Async versions of the same methods. Switching to using those, along with some downstream calls to the same package fixed the problem.
但是,使用 Visual Studio 的“设置下一条语句”功能,我系统地跳过了几行以确定是哪一行导致了它。我一直深入研究本地方法,直到调用外部 NuGet 包。被调用的方法将 anAction
作为参数,并且为此 Action 传入的 lambda 表达式前面是async
关键字。正如 Stephen Cleary 在上面的回答中指出的那样,这被视为async void
MVC 不喜欢的 。幸运的是,包有相同方法的 *Async 版本。切换到使用这些,以及对同一个包的一些下游调用解决了这个问题。
I realize this is not a novel solution to the problem, but I passed over this thread a few times in my searches trying to resolve the issue because I thought I didn't have any async void
or async <Action>
calls, and I wanted to help someone else avoid that.
我意识到这不是该问题的新颖解决方案,但我在尝试解决该问题的搜索中多次跳过此线程,因为我认为我没有任何电话async void
或async <Action>
电话,我想帮助其他人避免这种情况.