【不小心就会犯错】 .NET的ConcurrentDictionary,线程安全集合类

ConcurrentDictionary 是.NET 4.0里面新增的号称线程安全的集合类。

那么自然,我们会预期ConcurrentDictionary 中的代码是线程安全的(至少几个关键方法是线程安全的).

举个例子,使用者可能会预期GetOrAdd中的方法当Key不存在的时候只执行一次Add的委托,第二次调用GetOrAdd就应该直接取回刚才生成的值了.

参考一下以下代码:

复制代码
        public static  void Test()
{
var concurentDictionary = new ConcurrentDictionary<int, int>();

var w = new ManualResetEvent(false);
int timedCalled = 0;
var threads = new List<Thread>();
for (int i = 0; i < Environment.ProcessorCount; i++)
{
threads.Add(new Thread(() =>
{
w.WaitOne();
concurentDictionary.GetOrAdd(1, i1 =>
{
Interlocked.Increment(ref timedCalled);
return 1;
});
}));
threads.Last().Start();
}

w.Set();//release all threads to start at the same time
Thread.Sleep(100);
Console.WriteLine(timedCalled);// output is 4, means call initial 4 times
//Console.WriteLine(concurentDictionary.Keys.Count);
}
复制代码

GetOrAdd方法的定义就是按照Key获取一个Value,如果Key不存在,那么调用Func<T> 添加一个键值对.

按照ConcurrentDictionary的定义,  我预期这个Add应该只被调用一次

可是上面那段代码的运行结果表明, Interlocked.Increment(ref timedCalled); 被调用了4次,真是尴尬啊

 

用于初始化值的委托还真的是可以多次执行的,所以

  • 要么保证委托中的代码重复执行不会有问题
  • 要么使用线程安全的初始化方法,例如Lazy<T> 

 

复制代码
 public static void Test()
{
var concurentDictionary = new ConcurrentDictionary<int, int>();

var w = new ManualResetEvent(false);
int timedCalled = 0;
var threads = new List<Thread>();
Lazy<int> lazy = new Lazy<int>(() => { Interlocked.Increment(ref timedCalled); return 1; });
for (int i = 0; i < Environment.ProcessorCount; i++)
{
threads.Add(new Thread(() =>
{
w.WaitOne();
concurentDictionary.GetOrAdd(1, i1 =>
{
return lazy.Value;
});
}));
threads.Last().Start();
}

w.Set();//release all threads to start at the same time
Thread.Sleep(100);
Console.WriteLine(timedCalled);// output is 1
}
复制代码

 

附: 注释中也不说一下这个初始化方法会被多次调用,如果不是偶然遇到这个问题,估计永远都不知道

复制代码
 //
// Summary:
// Adds a key/value pair to the System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue>
// if the key does not already exist.
//
// Parameters:
// key:
// The key of the element to add.
//
// valueFactory:
// The function used to generate a value for the key
//
// Returns:
// The value for the key. This will be either the existing value for the key
// if the key is already in the dictionary, or the new value for the key as
// returned by valueFactory if the key was not in the dictionary.
//
// Exceptions:
// System.ArgumentNullException:
// key is a null reference (Nothing in Visual Basic).-or-valueFactory is a null
// reference (Nothing in Visual Basic).
//
// System.OverflowException:
// The dictionary contains too many elements.
复制代码

 

该集合类中所有使用Func<T>的方法也存在类似的问题

希望能给还不知道该问题的朋友提个醒,避免不必要的BUG

posted on   听说读写  阅读(22276)  评论(14编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?

导航

< 2011年11月 >
30 31 1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 1 2 3
4 5 6 7 8 9 10
点击右上角即可分享
微信分享提示