Web Api + HttpClient:异步模块或处理程序已完成,而异步操作仍处于挂起状态

Posted

技术标签:

【中文标题】Web Api + HttpClient:异步模块或处理程序已完成,而异步操作仍处于挂起状态【英文标题】:Web Api + HttpClient: An asynchronous module or handler completed while an asynchronous operation was still pending 【发布时间】:2013-02-25 04:38:37 【问题描述】:

我正在编写一个使用 ASP.NET Web API 代理一些 HTTP 请求的应用程序,并且我正在努力确定间歇性错误的来源。 这似乎是一种竞争条件......但我并不完全确定。

在我详细介绍之前,这里是应用程序的一般通信流程:

客户端代理 1 发出 HTTP 请求。 代理 1 将 HTTP 请求的内容中继到代理 2 代理 2 将 HTTP 请求的内容中继到目标 Web 应用程序 目标 Web 应用响应 HTTP 请求并将响应流式传输(分块传输)到代理 2 Proxy 2 将响应返回给 Proxy 1,后者又响应原始调用 Client

代理应用程序是使用 .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;

间歇性出现的错误是:

异步模块或处理程序已完成,而异步操作仍处于挂起状态。

此错误通常发生在对代理应用程序的前几次请求中,之后该错误不再出现。

Visual Studio 永远不会在抛出异常时捕获异常。 但是可以在 Global.asax Application_Error 事件中捕获错误。 不幸的是,异常没有堆栈跟踪。

代理应用程序托管在 Azure Web 角色中。

任何帮助确定罪魁祸首将不胜感激。

【问题讨论】:

CopyHeaders 是我编写的一种方法,用于中继我认为适合为我的应用程序复制的 HTTP 标头。它没有包括在这里,因为它与我试图解决的问题无关。我最终得到的解决方案类似于下面接受的答案,应该足以让您编写类似的解决方案。 【参考方案1】:

您的问题是一个微妙的问题:您传递给PushStreamContentasync lambda 被解释为async void(因为PushStreamContent constructor 仅将Actions 作为参数)。因此,您的模块/处理程序完成与 async void lambda 完成之间存在竞争条件。

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;

如果您希望代理更好地扩展,我还建议您摆脱所有 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;
  

您以前的代码会为每个请求阻塞一个线程(直到收到标头);通过使用async 一直到您的控制器级别,您不会在此期间阻塞线程。

【讨论】:

有趣 - 我会调查您的解决方案并回复您。我知道我可以使用 Async 进行一些性能改进 - 这是我消除这个特殊问题后的下一个挑战。 嗨,斯蒂芬,您的解决方案不会关闭错误的流吗?它关闭(通过处置)传入流而不是传出内容流,因此请求永远不会结束(直到超时)。 @Stephen copyToStreamAsync 的执行返回一个Task,所以var a 实际上是Task a,对吗?我看不到任务完成的任何保证。相反,我会将结构更改为var content = new PushStreamContent((stream, h, t) =&gt; copyToStreamAsync(stream).Wait());(显然,.Wait 是要避免的,但在混合异步和同步时似乎有时是必要的。)那么为什么你的工作?我发布的代码如何比较? @DanFriedman:_Task,是的。 _CopyToAsync 完成后完成。关键是当stream 关闭时PushStreamContent 将被视为“完整”。所以你的代码只有在copyToStreamAsync(stream) 关闭stream 时才能工作。与Wait 的主要区别在于它会阻塞线程(并可能导致死锁)。 这对我有帮助。我没有过多地使用async,但这里的教训似乎是您应该始终将async 方法的返回类型设置为TaskTask&lt;&gt;,这让我认为语言应该需要这个。我的代码看起来不像 OP 的代码,但我有一个 async void 并将其更改为 async Task 解决了这个问题。【参考方案2】:

我想为遇到同样错误的其他人补充一些智慧,但您的所有代码似乎都很好。在发生这种情况的调用树中查找传递给函数的任何 lambda 表达式。

我在对 MVC 5.x 控制器操作的 javascript JSON 调用中收到此错误。我在堆栈上下所做的一切都是定义async Task 并使用await 调用的。

但是,使用 Visual Studio 的“设置下一条语句”功能,我系统地跳过了几行以确定是哪一个导致了它。我一直深入研究本地方法,直到调用外部 NuGet 包。被调用的方法将Action 作为参数,并且为此Action 传入的lambda 表达式前面有async 关键字。正如 Stephen Cleary 在上面的回答中指出的那样,这被视为 async void,这是 MVC 不喜欢的。幸运的是,包具有相同方法的 *Async 版本。切换到使用这些,以及对同一个包的一些下游调用解决了这个问题。

我意识到这不是解决问题的新方法,但我在尝试解决问题的搜索中跳过了几次此线程,因为我认为我没有任何 async voidasync &lt;Action&gt; 电话,我想帮助其他人避免这种情况。

【讨论】:

优秀的答案。这个问题真的让我摸不着头脑,这个答案帮助我指出了问题的方向。在我的情况下,我使用 MVC Web API 并从构造函数调用异步方法。使构造函数调用非异步方法解决了这个问题。【参考方案3】:

一个稍微简单的模型是,您实际上可以直接使用 HttpContents 并在中继内部传递它们。我刚刚上传了一个示例,说明了如何以相对简单的方式异步依赖请求和响应,而无需缓冲内容:

http://aspnet.codeplex.com/SourceControl/changeset/view/7ce67a547fd0#Samples/WebApi/RelaySample/ReadMe.txt

重用相同的 HttpClient 实例也是有益的,因为这允许您在适当的情况下重用连接。

【讨论】:

感谢 Henrik,这实际上是我最终提出的解决方案。由于我们中继的一些细节,我可以在通过我们系统的 90% 的路由中使用它 - 但我必须依靠 PushContent 响应来处理其余的。

以上是关于Web Api + HttpClient:异步模块或处理程序已完成,而异步操作仍处于挂起状态的主要内容,如果未能解决你的问题,请参考以下文章

使用 Windows.Web.Http.HttpClient 类 PATCH 异步请求

C#.NET Web API 2,如何使用 HTTPGET 异步方法上的 HttpContent 从网站中提取特定文本?

如何在 .net Core API 项目中跨多个线程限制对 HttpClient 的所有传出异步调用

如何使用 HttpClient 从 Web Api 调用 PUT 方法?

使用 HttpClient 和 Web API 方法 [FromBody] 参数发布到 Web API 最终为空

使用 libev 的异步 HttpClient [关闭]