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();
}

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

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

4

2 回答 2

10

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

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

var sr = new StreamReader(ms);
responseContent = sr.ReadToEnd();
于 2014-12-04T18:07:26.850 回答
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 中挂起?

于 2013-10-22T17:44:50.817 回答