通用技术 TValue ConcurrentDictionary<TKey,TValue>.GetOrAdd (TKey key,Func<TKey,TValue> valueFactory)

SinDynasty · 2018年05月03日 · 最后由 陈子昂 回复于 2018年05月03日 · 1811 次阅读

ConcurrentDictionary<TKey,TValue>

ConcurrentDictionary<TKey,TValue>是在.Net 基础类库 System.Collections.Concurrent 命名空间下的一个可由多个线程同时访问的线程安全的键值对集合,其基于原子操作,并使用细粒度锁定,确保了修改、写入等方法的线程安全。

但是这真的是这样吗?

如果你去看官方MSDN中有关这个类的文档时,你会在最后看到这么一句话:All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary<TKey, TValue> class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd.

其翻译过来的意思是指 ConcurrentDictionary<TKey,TValue>类中的 TValue GetOrAdd(TKey key,Func<TKey,TValue> valueFactory) 和 TValue AddOrUpdate(TKey key,Func<TKey,TValue> addValueFactory,Func<TKey,TValue,TValue> updateValueFactory) 方法是个例外。

其原因就是因为 Func<TKey,TValue> valueFactor 和 Func<TKey,TValue,TValue> updateValueFactor 并不是基于原子操作的。

TValue GetOrAdd(TKey key,Func<TKey,TValue> valueFactory)

以 ConcurrentDictionary<TKey,TValue>类中的 TValue GetOrAdd(TKey key,Func<TKey,TValue> valueFactory) 方法为例,其内部的逻辑大致是这样的:首先判断传入的 Key 值是否已经存在,如果存在则返回相应的 Value,如果不存在则调用 valueFactor 委托并传入 Key 值,生成相应的 Value 值,然后再次判断传入的 Key 值是否存在,如果存在则返回已经生成的 Value 值,如果不存在,则使用原子操作(InterLocked)的方式,将其添加进集合中。

很明显由于其在调用 valueFactor 委托来生成相应的 Value 值时,并未加锁,这就使得其在多线程情况下可能调用 2 次 valueFactor 委托来生成相应的对象。

其情景如下:

1、线程 A 传入 Key 值,此时判断传入的 Key 值并不存在于集合中,于是调用 valueFactor 委托来生成 ValueA;

2、线程 B 传入与线程 A 相同的 Key 值,由于多线程的原因,此时也判断传入的这个 Key 并不存在与集合中,于是调用 valueFactor 委托来生成 ValueB;

3、线程 A 首先完成 valueFactor 委托,从而获得了 ValueA,此时线程 A 再次判断 Key 是否存在在集合中,得到的结果是不存在,因此将这 Key 值和 ValueA 值以原子操作的方式添加进集合;

4、线程 B 后完成 valueFactor 委托,获得了 ValueB,此时线程 B 判断集合中已经存在了 Key 值,于是舍弃了已经生成的 ValueB 值,并返回由线程 A 生成的 ValueA 值;

结论:很明显,由于程序在调用 valueFactor 委托时,并不是线程安全的,这就导致了 valueFactor 委托可能会被调用 2 次或多次,因此可以说 ConcurrentDictionary<TKey,TValue>类中的 TValue GetOrAdd(TKey key,Func<TKey,TValue> valueFactory) 方法并不是完全的线程安全的。

备注:ConcurrentDictionary<TKey,TValue>类中的 TValue AddOrUpdate(TKey key,Func<TKey,TValue> addValueFactory,Func<TKey,TValue,TValue> updateValueFactory) 方法也是同理。

优化

Lazy<T>类是一个在.Net 基础类库 System 命名空间下的一个提供对象延迟初始化的类。

我们使用构造函数 Lazy<T> (Func<T>) 创建出一个 Lazy<T>的实例对象后,其并不会立即初始化出一个类型 T 的实例,其只会在我们第一获取 Lazy<T>的 Value 属性时,实例化出一个类型为 T 的对象,并在之后获取 Value 属性时,得到的都是第一次获取所生成的对象,并且在其过程中保证了线程的安全。

因此我们就可以将两者结合起来,使用 ConcurrentDictionary<TKey,Lazy<TValue>>类来实现完全的线程安全,并且由于 Lazy<T>的延迟初始化的特性使得类型 T 必定只会初始化一次,这也就意味着当我们如果要初始化一个非常耗性能的类时,我们可以使用这种方法来优化性能。

共收到 1 条回复 时间 点赞

萌兽写得不错,不过我只能看得懂一部分,我写 unity 前端方法的,和这个有些差异。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册