.NET:100% CPU 在 HttpClient 中的使用是因为 Dictionary?

.NET: 100% CPU usage in HttpClient because of Dictionary?

小问题:
有没有其他人在使用单例 .NET HttpClient 时遇到问题,其中应用程序将处理器固定在 100%,直到它重新启动?

详情:
我是 运行 一个 Windows 连续的服务,schedule-based ETL。 data-syncing 线程之一偶尔会死掉,或者开始 运行 失控并将处理器固定在 100%。

我很幸运在有人简单地重新启动服务(标准修复)之前看到了这种情况,并且能够获取 dump-file。

在 WinDbg(带 SOS 和 SOSEX)中加载它,我发现我有大约 15 个线程(sub-tasks 主处理线程)全部 运行 完全相同 stack-traces。但是,似乎没有任何僵局。 IE。 high-utilization 个线程 运行,但从未完成。

相关的 stack-trace 部分如下(地址省略):

System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].TryGetValue(System.__Canon, System.__Canon ByRef)
System.Net.Http.Headers.HttpHeaders.ContainsParsedValue(System.String, System.Object)
System.Net.Http.Headers.HttpGeneralHeaders.get_TransferEncodingChunked()
System.Net.Http.Headers.HttpGeneralHeaders.AddSpecialsFrom(System.Net.Http.Headers.HttpGeneralHeaders)
System.Net.Http.Headers.HttpRequestHeaders.AddHeaders(System.Net.Http.Headers.HttpHeaders)
System.Net.Http.HttpClient.SendAsync(System.Net.Http.HttpRequestMessage, System.Net.Http.HttpCompletionOption, System.Threading.CancellationToken)
...
[Our Application Code]

根据 this article(以及我发现的其他人),字典的使用是 而不是 thread-safe,并且无限循环是可能的(如是 straight-up 崩溃)如果你以 multi-threaded 方式访问字典。

但是 我们的应用程序代码没有明确使用字典。那么stack-trace中提到的词典在哪里呢?

通过 .NET Reflector,出现 HttpClient 使用字典来存储已在 "DefaultRequestHeaders" 属性 中配置的任何值.因此,通过 HttpClient 发送的任何请求都会触发单例非 thread-safe 字典的枚举(以便将默认值 headers 添加到请求中),这可能会无限旋转(或kill) 发生损坏时涉及的线程。

微软直言HttpClientclass是thread-safe。但在我看来,如果任何 headers 已添加到 HttpClient.

的 DefaultRequestHeaders 中,这就不再正确了

我的分析似乎表明这是真正的根本问题,一个简单的解决方法就是永远不要使用 DefaultRequestHeaders,因为 HttpClient 可以 multi-threaded 方式使用。

但是,我正在寻找一些证据来证明我没有找错树。如果这是正确的,这似乎是 .NET 框架中的一个错误,我自然而然地倾向于怀疑。

抱歉这个冗长的问题,但感谢您的任何意见。

感谢所有评论;他们让我从不同的角度思考问题,并帮助我找到了问题的根本原因。

虽然问题 DefaultRequestHeaders 支持字典损坏的结果,但真正的罪魁祸首是 HttpClient 对象的初始化代码:

private HttpClient InitializeClient()
{
    if (_client == null)
    {
        _client = GetHttpClient();
        _client.DefaultRequestHeaders.Accept.Clear();
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        SetBaseAddress(BaseAddress);
    }
    return _client;
}

我说过 HttpClient 是一个单例,这是部分不正确的。它是作为单个实例创建的,在执行一个工作单元的多个线程之间共享,并在工作完成时被释放。下次必须完成此特定任务时,将启动一个新实例。

上面的 "InitializeClient" 方法在每次发送请求时都会被调用,并且应该只是因为“_client”字段在第一次 运行-through 之后不为空而短路.

(请注意,这不是在对象的构造函数中完成的,因为它是一个抽象 class,而 "GetHttpClient" 是一个抽象方法——顺便说一句:永远不要调用抽象方法在 base-class 的构造函数中...导致其他噩梦)

当然,很明显这不是线程安全的,并且由此产生的行为是不确定的。

解决方法是将此代码放在经过双重检查的 "lock" 语句后面(尽管我无论如何都会消除 "DefaultRequestHeaders" 属性 的使用,只是因为)。

简而言之,如果您在初始化 HttpClient 时小心谨慎,我最初的问题应该不会成为问题。

感谢大家提供的思路清晰!