推荐:
Pocket Bookmarks。 口袋书签
谷歌浏览器插件:立即安装 Pocket Bookmarks
edge浏览器插件:立即安装Pocket Bookmarks
为什么你急需这个插件?
3秒极简操作:无需学习成本,清爽界面一键管理
跨设备无缝同步:电脑/手机随时存取重要链接
黑科技AI助手:自动分类+智能推荐,比你自己更懂你的收藏习惯
可视化数据看板:TOP10常用书签、访问趋势一目了然
效率党最爱的功能:
- 多维度分类:支持标签+文件夹双重管理
- 智能排序:按访问频率/创建时间快速筛选
- 团队协作:分类书签一键共享给同事
- 个性展示:九宫格/列表/时间轴多种视图
在一次线上接口性能异常的排查中,我们遇到了一个非常典型但又常被忽视的问题 ------
在多线程任务中并发操作 Dictionary,导致 CPU 飙升并触发 Dictionary.FindEntry 的热点。
本文将完整复现问题背景、分析原因,并给出最终可落地的解决方案,帮助你避免类似的踩坑。
🧩 一、问题背景
线上某接口突然出现大量 CPU 占用过高的告警,通过 dump 分析后,发现大量线程卡在:
System.Collections.Generic.Dictionary.FindEntry
如下图所示(简化后):
Dictionary<TKey, TValue>.FindEntry
OrderMainService.QueryPrice
UnifiedPriceService.GetUnifiedPrice
Task.Run(...)
...
进一步追踪代码,发现在一个方法内创建了多个 Task.Run,并在任务中同时对同一个字典 unifiedPriceMap 进行 Add、ContainsKey、索引访问 等操作:
var unifiedPriceMap = new Dictionary<SupplierTypeEnum, T3EstimatePriceResultDtoModel>();
taskList.Add(Task.Run(() =>
{
unifiedPriceMap.Add(SupplierTypeEnum.XieHua, model);
}));
taskList.Add(Task.Run(() =>
{
unifiedPriceMap.Add(SupplierTypeEnum.JuZi, model);
}));
看似简单,却埋下了灾难的种子。
🔥 二、问题分析:并发写 Dictionary 会导致结构损坏
.NET 中 Dictionary<TKey, TValue> 不是线程安全的 。
只要有多个线程同时向同一个 Dictionary 写入,就会出现:
-
buckets 与 entries 同时被多个线程修改
-
entry 链表被截断
-
next 索引形成闭环
-
FindEntry 死循环
-
Dictionary 内部结构损坏
-
CPU 迅速飙升
其中 FindEntry 高 CPU 正是最典型的表现。
也就是 dump 中看到的这个热点:
Dictionary<TKey, TValue>.FindEntry
✔ 为什么读写会冲突?
为了添加新元素,Dictionary 会:
-
计算 hash
-
修改 bucket
-
修改 entry 数组
-
修改 entry.next
-
扩容时对整个结构整体重排
在多线程写入时,同时执行上述步骤,非常容易造成:
-
bucket 指针错链
-
next 指针循环
-
entries 覆盖
-
甚至内部 Resize 时数组损坏
最终导致 CPU 占用不断攀升。
这正是你观察到的现象。
🛠 三、解决方案
根据实际情况,可以用三种方式解决该问题。
✅ 方案一:使用 ConcurrentDictionary(最简单、最安全)
最推荐的方案,只需一行代码即可修复所有并发问题:
var unifiedPriceMap = new ConcurrentDictionary<SupplierTypeEnum, T3EstimatePriceResultDtoModel>();
写入方式:
unifiedPriceMap[SupplierTypeEnum.XieHua] = model;
优点:
-
原生线程安全
-
无需加锁
-
性能表现稳定
-
完全规避 Dictionary 结构损坏问题
这是最通用、最易落地的方案。
✅ 方案二:加锁保护 Dictionary(性能更高)
如果你的字典 Key 很少(如供应商就几个),加锁反而更高效:
var locker = new object();
var unifiedPriceMap = new Dictionary<SupplierTypeEnum, T3EstimatePriceResultDtoModel>();
taskList.Add(Task.Run(() =>
{
var value = service.GetUnifiedPrice(...);
lock(locker)
{
unifiedPriceMap[SupplierTypeEnum.XieHua] = value;
}
}));
优点:
-
Dictionary 性能极高
-
加锁范围很小(只包含写入)
缺点:
- 比 ConcurrentDictionary 稍微麻烦一些
❌ 方案三(不推荐):每个 Task 使用局部变量,最后合并
可行,但代码复杂,不够优雅。
🎯 四、经验总结
1. Dictionary 只能在单线程下写、并发读
只要多线程写,100% 会出问题,迟早都崩。
2. ConcurrentDictionary 是并发写字典的标准解决方案
现代 .NET 并发场景下,应优先使用它。
3. 如果写入频次不高,用 lock 更快
对小数据量、固定 Key 来说,加锁的性能甚至比 ConcurrentDictionary 更高。
4. FindEntry 热点是字典结构损坏的第一现场
只要 dump 看到 Dictionary.FindEntry 高 CPU,基本可以断定是并发写 Dictionary。
🧭 五、结语
这次问题看似简单,但却是 .NET 项目中非常高频、又极易被忽略的典型并发 bug。
并发写 Dictionary = 不定时炸弹
只要做到:
-
并发写 → 用 ConcurrentDictionary 或 lock
-
单线程写、多线程读 → 用 Dictionary
你就能完全规避这类性能事故。
如果你愿意,我也可以进一步帮你:
-
对整个方法进行并发结构重构
-
优化 Task 性能
-
做缓存/批处理方案提升性能
-
进一步分析 dump 和热点调用栈