HttpContent.ReadAsStringAsync导致请求挂起(或出现其他奇怪行为)

11

我们正在构建一个高度并发的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();
}

现在,在调用content.ReadAsStringAsync()之后,HttpContext.Current为null,并且在所有后续请求中它始终为null !我知道这听起来难以置信——我花了一些时间,有三个同事在场才接受了这个事实。

这是一种预期的行为吗?我做错了什么吗?


2
你要明白,调用 ReadContentAsStringAsync 然后立即在其上调用 Result 实际上就是抵消了异步性,对吧?这样会阻塞直到任务完成。而且,在 await 之后 HttpContext.Current 变为 null 的问题听起来就像是它没有在 await 点传递过去一样,这真让人恼火,但也不完全令我惊讶。你可以在异步方法的开始处获取它并使用局部变量... - Jon Skeet
2
你确定总是可以读取 HttpResponseMessage.Content 吗?在我看来,试图读取自己的输出流可能不被支持。 - Stephen Cleary
@alextercete:我了解有些限制,其中输入内容只能读取一次。所以你确定读取输出内容会起作用吗?因为ASP.NET在你之后必须再次读取它。 - Stephen Cleary
如果您一直在使用ObjectContent,我建议尝试读取Value而不是将其作为流进行阻塞异步读取。 - Stephen Cleary
@alextercete,如果您不想失去HttpContext.Current,请将其保存在“本地”变量中或将其作为参数传递给您的方法。 - Paulo Morgado
显示剩余5条评论
2个回答

11

我曾经遇到过这个问题。虽然我还没有完全测试,但是使用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路由到我的方法时,参数为null。我通过使用ReadAsStreamAsync来解决它,然后在完成后将其移回流的开头。但是,使用这种方法,您必须使用带有5个参数的StreamReader构造函数,并将最后一个参数设置为“true”以保持底层流处于打开状态。 - esteuart

2
关于您的第二个问题,async/await是编译器构建状态机的语法糖,在以"await"为前缀的函数调用上返回当前线程...其中包含HttpContext.Current在其线程本地存储中。该异步调用的完成可能发生在一个不同的线程上...其中不包含HttpContext.Current在其线程本地存储中。
如果您希望完成在同一线程上执行(因此具有与HttpContext.Current相同的线程本地存储对象),则需要注意此行为。这在从主UI线程(如果您正在构建Windows应用程序)或在ASP.NET中,从依赖于HttpContext.Current的ASP.NET请求线程中进行调用时尤其重要。
请参阅ConfigureAwait(false)的参考文档。此外,请查看一些TPL的Channel 9教程。一旦掌握了“简单”的内容,演示者必然会谈论此问题,因为它会引起微妙的问题,除非您知道TPL在内部做了什么,否则很难理解。
祝你好运。
关于您的第一个问题,如果调用方获得结果,我不确定IIS是否已完成请求。您如何确定由此调用方启动的ASP.NET请求线程在IIS中挂起?

哇!这非常有帮助。我从来没有想到过这可能是个问题,但现在看起来很合理。 - John Henckel

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