【问题标题】:HttpContent.ReadAsStringAsync causes request to hang (or other strange behaviours)HttpContent.ReadAsStringAsync 导致请求挂起(或其他奇怪的行为)
【发布时间】:2013-07-21 18:11:51
【问题描述】:

我们正在构建一个高度并发的 Web 应用程序,最近我们开始广泛使用异步编程(使用 TPL 和async/await)。

我们有一个分布式环境,其中应用程序通过 REST API(构建在 ASP.NET Web API 之上)相互通信。在一个特定的应用程序中,我们有一个DelegatingHandler,它在调用base.SendAsync 之后(即在计算响应之后)将响应记录到文件中。我们在日志中包含响应的基本信息(状态码、标头和内容):

public static string SerializeResponse(HttpResponseMessage response)
{
    var builder = new StringBuilder();
    var content = ReadContentAsString(response.Content);

    builder.AppendFormat("HTTP/{0} {1:d} {1}", response.Version.ToString(2), response.StatusCode);
    builder.AppendLine();
    builder.Append(response.Headers);

    if (!string.IsNullOrWhiteSpace(content))
    {
        builder.Append(response.Content.Headers);

        builder.AppendLine();
        builder.AppendLine(Beautified(content));
    }

    return builder.ToString();
}

private static string ReadContentAsString(HttpContent content)
{
    return content == null ? null : content.ReadAsStringAsync().Result;
}

问题是这样的:当代码到达content.ReadAsStringAsync().Result 在服务器负载很重的情况下,请求有时会在IIS 上挂起。当它返回时,它有时会返回响应——但在 IIS 上挂起,就好像它没有返回一样——或者在其他时候它永远不会返回。

我也尝试使用ReadAsByteArrayAsync 读取内容,然后将其转换为String,但没有成功。

当我将代码转换为始终使用异步时,我会得到更奇怪的结果:

public static async Task<string> SerializeResponseAsync(HttpResponseMessage response)
{
    var builder = new StringBuilder();
    var content = await ReadContentAsStringAsync(response.Content);

    builder.AppendFormat("HTTP/{0} {1:d} {1}", response.Version.ToString(2), response.StatusCode);
    builder.AppendLine();
    builder.Append(response.Headers);

    if (!string.IsNullOrWhiteSpace(content))
    {
        builder.Append(response.Content.Headers);

        builder.AppendLine();
        builder.AppendLine(Beautified(content));
    }

    return builder.ToString();
}

private static Task<string> ReadContentAsStringAsync(HttpContent content)
{
    return content == null ? Task.FromResult<string>(null) : content.ReadAsStringAsync();
}

现在HttpContext.Current 在调用content.ReadAsStringAsync() 后为空,对于所有后续请求,它一直为空!我知道这听起来令人难以置信——我花了一些时间和三位同事的在场才接受这真的发生了。

这是某种预期的行为吗?我在这里做错了吗?

【问题讨论】:

  • 您确实意识到调用ReadContentAsStringAsync 然后立即调用Result 基本上是在否定异步,对吧?这将阻塞,直到作业完成。而HttpContext.Current 在等待之后成为null 听起来好像它只是没有流过await 点,这很烦人,但并没有完全让我感到惊讶。您可以在异步方法的 start 处获取它,然后使用该局部变量...
  • 在尝试处理内容之前,我可能会在调用 response.EnsureSuccessStatusCode() 之前调用 wrap a try catch。
  • 你确定总是可以阅读HttpResponseMessage.Content吗?在我看来,可能不支持尝试读取您自己的输出流。
  • @JonSkeet,是的,我知道这一点。我“阻止”该操作的唯一原因是试图避免丢失HttpContext.Current“永远”——我做到了,但随后出现了悬而未决的问题。顺便说一句,我认为您的理解不正确,但 HttpContext.Current 在 HTTP 请求结束之前不会丢失。后续 HTTP 请求会丢失它。我只能通过iisreset 找回它。
  • @StephenCleary,你这是什么意思?就我而言,我不是直接将流写入response.Content,而是使用ObjectContent

标签: c# async-await c#-5.0 httpcontent


【解决方案1】:

我遇到了这个问题。虽然,我还没有完全测试,但使用 CopyToAsync 而不是 ReadAsStringAsync 似乎可以解决问题:

var ms = new MemoryStream();
await response.Content.CopyToAsync(ms);
ms.Seek(0, SeekOrigin.Begin);

var sr = new StreamReader(ms);
responseContent = sr.ReadToEnd();

【讨论】:

  • 当我尝试这个时,我能够读取流进行安全检查,但是当 WebApi 路由到我的方法时,参数为空。我通过使用 ReadAsStreamAsync 来解决它,然后在完成后移回流的开头。但是,使用这种方法,您必须使用带有 5 个参数的 StreamReader 构造函数并使最后一个参数为“真”以保持底层流打开。
【解决方案2】:

关于您的第二个问题,async/await 是编译器构建状态机的语法糖,其中对“await”前面的函数的调用立即在当前线程上返回......一个包含 HttpContext 的线程。当前在其线程本地存储中。该异步调用的完成可能发生在一个不同的线程上...一个线程本地存储中没有 HttpContext.Current 的线程。

如果您希望完成在同一个线程上执行(因此在线程本地存储中具有相同的对象,如 HttpContext.Current),那么您需要注意这种行为。这对于来自主 UI 线程(如果您正在构建 Windows 应用程序)或在 ASP.NET 中的调用(来自依赖 HttpContext.Current 的 ASP.NET 请求线程的调用)尤其重要。

请参阅有关 ConfigureAwait(false) 的参考文档。此外,还可以查看有关 TPL 的一些 Channel 9 教程。一旦“简单”的东西被摸透了,演示者总是会谈论这个问题,因为它会导致不容易理解的微妙问题,除非你知道 TPL 在幕后做什么。

祝你好运。

关于你的第一个问题,如果调用者得到结果,我不相信 IIS 没有完成请求。您如何确定由该调用方发起的 ASP.NET 请求线程在 IIS 中挂起?

【讨论】:

  • 天哪!这很有帮助。我从来没有想到这可能是一个问题,但这是有道理的。
猜你喜欢
  • 2023-03-11
  • 1970-01-01
  • 2019-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-27
  • 1970-01-01
相关资源
最近更新 更多