谨慎使用 ConcurrentDictionary.Values
谨慎使用 C# 中的 ConcurrentDictionary.Values
在多线程开发中,ConcurrentDictionary
是一个非常重要的数据结构,它提供了线程安全的字典操作。然而,在使用其 Values
属性时,我们需要格外小心,特别是在处理大数据量的场景中。本文通过一个示例程序分析了 ConcurrentDictionary.Values
的潜在问题,并探讨了优化方案。
-
问题描述
以下是一个简单的示例程序,它展示了在多线程环境中频繁调用
ConcurrentDictionary.Values
时的内存波动现象:internal class Program { static void Main(string[] args) { Parallel.For(1, 100000, i => { Test(); Console.WriteLine($"第{i}次调用"); }); Console.WriteLine("Hello, World!"); Console.ReadLine(); } public static void Test() { var query = CacheHelper.GetAll(); Console.WriteLine($"{query.Count}"); Thread.Sleep(100); } } public class CacheHelper { static ConcurrentDictionary<string, string> allDic = new ConcurrentDictionary<string, string>(); static CacheHelper() { for (int i = 0; i < 80000; i++) { allDic.TryAdd(i.ToString(), string.Join(",", Enumerable.Range(0, 500))); } } public static ICollection<string> GetAll() { return allDic.Values; } }
-
现象分析
运行上述代码后,可以观察到程序内存占用不断上升,达到一个高峰后,内存被回收,但随后继续增长。这种内存波动在处理大字符串时尤为明显。通过
dotMemory
查看内存情况,如上图 -
源码分析
通过查看
ConcurrentDictionary
的源码,可以清楚地理解Values
属性的工作机制:private ReadOnlyCollection<TValue> GetValues() { int locksAcquired = 0; try { AcquireAllLocks(ref locksAcquired); int countNoLocks = GetCountNoLocks(); if (countNoLocks == 0) { return ReadOnlyCollection<TValue>.Empty; } TValue[] array = new TValue[countNoLocks]; int num = 0; VolatileNode[] buckets = _tables._buckets; for (int i = 0; i < buckets.Length; i++) { VolatileNode volatileNode = buckets[i]; for (Node node = volatileNode._node; node != null; node = node._next) { array[num] = node._value; num++; } } return new ReadOnlyCollection<TValue>(array); } finally { ReleaseLocks(locksAcquired); } }
-
关键点
-
每次调用
Values
都会重新生成一个新数组:TValue[] array = new TValue[countNoLocks];
这意味着每次获取
Values
都会创建一个新的TValue[]
,而不是返回ConcurrentDictionary
内部的引用。这可能是为了线程安全而设计的,但在高并发场景下会导致频繁的内存分配。 -
存储对象的大小和数量会加剧问题:
在示例程序中,ConcurrentDictionary
存储了大量的长字符串。这使得每次调用Values
时,生成的临时数组占用大量内存,GC 回收的压力显著增加。 -
早期版本的实现对比:
在 .NET 5 中,类似的逻辑使用了List<TValue>
,其本质行为与当前版本一致,依然会重新创建一个临时容器。 -
场景优化建议
针对上述问题,我们可以采取以下优化方案:
-
1. 避免频繁调用
ConcurrentDictionary.Values
在数据量较大或高并发场景中,尽量避免直接使用
ConcurrentDictionary.Values
。根据具体需求,设计更高效的数据访问方式。 -
2. 使用
lock
+Dictionary
替代Dictionary
本身不是线程安全的,但其Values
属性返回的是字典内部的引用,而不会重新分配内存。在某些场景下,可以采用lock
+Dictionary
替代。示例代码如下:
public class CacheHelper { private static Dictionary<string, string> allDic = new Dictionary<string, string>(); private static readonly object lockObj = new object(); static CacheHelper() { for (int i = 0; i < 80000; i++) { allDic.Add(i.ToString(), string.Join(",", Enumerable.Range(0, 500))); } } public static ICollection<string> GetAll() { lock (lockObj) { return allDic.Values; } } }
通过这种方式,我们可以避免每次调用
Values
时分配大量新对象,同时保证线程安全。
-
总结
ConcurrentDictionary
是一个强大的线程安全数据结构,但在高并发、大数据量的场景下,使用其Values
属性时需特别注意。通过了解其底层实现和内存分配机制,我们可以采取以下优化策略:- 减少
Values
的调用频率,避免频繁分配临时内存。 - 在合适的场景下使用
lock
+Dictionary
替代,既能保证线程安全,又能减少 GC 压力。
合理利用工具(如
dotMemory
)分析内存行为,将有助于定位和优化类似问题。 - 减少
-
## 参考链接
https://www.cnblogs.com/huangxincheng/p/15329098.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤