謹慎使用 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; } } -
現象分析
![內存.png]()
運行上述代碼后,可以觀察到程序內存占用不斷上升,達到一個高峰后,內存被回收,但隨后繼續增長。這種內存波動在處理大字符串時尤為明顯。通過
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)分析內存行為,將有助于定位和優化類似問題。 - 減少


浙公網安備 33010602011771號