.NET:由于Dictionary,HttpClient中的CPU使用率是100%?

Gre*_*ght 8 .net c# multithreading dictionary dotnet-httpclient

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

详细信息:
我正在运行一个Windows服务,它可以执行基于计划的连续ETL.其中一个数据同步线程偶尔会死掉,或者开始失控,并将处理器固定在100%.

我很幸运能够在有人重新启动服务(标准修复程序)之前看到这种情况发生,并且能够获取转储文件.

在WinDbg(带有SOS和SOSEX)中加载它,我发现我有大约15个线程(主处理线程的子任务)都运行相同的堆栈跟踪.但是,似乎没有任何死锁.IE高利用率线程正在运行,但从未完成.

相关的堆栈跟踪段如下(地址省略):

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]
Run Code Online (Sandbox Code Playgroud)

根据这篇文章(以及我发现的其他文章),字典的使用不是线程安全的,如果以多线程方式访问字典,则无限循环是可能的(如直接崩溃).

我们的应用程序代码没有明确使用字典.那么堆栈跟踪中提到的字典在哪里?

通过.NET Reflector,看起来 HttpClient使用字典来存储已在"DefaultRequestHeaders"属性中配置的任何值.因此,通过HttpClient发送的任何请求都会触发单个非线程安全字典的枚举(为了将默认标头添加到请求中),这可能会无限地旋转(或终止)所涉及的线程.发生腐败.

微软直言不讳地说HttpClient类是线程安全的.但在我看来,如果已将任何标头添加到HttpClient的DefaultRequestHeaders中,则不再适用.

我的分析似乎表明这是真正的根本问题,一个简单的解决方法是简单地从不使用可以以多线程方式使用HttpClient的DefaultRequestHeaders.

但是,我正在寻找一些确认,我没有咆哮错误的树.如果这是正确的,它似乎是.NET框架中的一个错误,我自然会怀疑.

对于罗嗦的问题感到抱歉,但感谢你提出的任何意见.

Gre*_*ght 4

感谢所有的评论; 他们让我沿着不同的思路思考,并帮助我找到问题的最终根源。

尽管该问题由 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;
}
Run Code Online (Sandbox Code Playgroud)

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

每次发送请求时都会调用上面的“InitializeClient”方法,并且应该由于第一次运行后“_client”字段不为空而短路。

(请注意,这不是在对象的构造函数中完成的,因为它是一个抽象类,并且“GetHttpClient”是一个抽象方法 - 顺便说一句:永远不要在基类的构造函数中调用抽象方法......这会导致其他噩梦)

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

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

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

感谢你们提供的清晰思路!