ConcurrentDictionary是.net BCL的一个线程安全的字典类,由于其方法的线程安全性,使用时无需手动加锁,被广泛应用于多线程编程中。然而,有的时候他们并不是如我们预期的那样工作。

拿它的一个GetOrAdd方法为例, 它的定义如下:

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

这是一个非常常用的方法,MSDN对它的描述为: 需要检索指定键的现有值,如果此键不存在,则需要指定一个键/值对。其行为模式是:

  • 第一次调用的时候会调用valueFactory创建值并返回
  • 后续调用的线程会直接返回字典中的检索值,valueFactory不会执行。

也就是说valueFactory只会在第一次调用的时候执行。由于微软在MSDN中说明这个函数是线程安全的,我一直以为其在并发执行的时候行为也是一样的,认为valueFactory只会执行一次。并且它也运行结果也一直如我所预期,然而今天定位一个问题的时候,通过日志发现其valueFactory是会执行多次的。

为了简单的展示这个问题,我这里写了一段简单的代码。

var dic = new ConcurrentDictionary<int, int>();

for (int i = ; i < ; i++)
{
runInNewThread(i);
} void runInNewThread(int i)
{
var thread = new Thread(para => dic.GetOrAdd(, _ => getNum((int)para)));
thread.Start(i);
} int getNum(int i)
{
Console.WriteLine($"Factory invoke. got {i}");
return i;
}

执行这段代码,结果如下:

Factory invoke. got 1
Factory invoke. got 4
Factory invoke. got 2
Factory invoke. got 0
Factory invoke. got 3
Factory invoke. got 5

也就是说,其valueFactory函数getNum是执行了6次的,并不是和我预期的结果一样的。便回头翻了下MSDN,发现MSDN在文章如何:在 ConcurrentDictionary 中添加和移除项中描述了这个现象。

简单的讲,微软设计这个函数时,将其设计成了线程安全的,但不是原子的。也就是说,微软的这个函数实现的方式是

lock (getOperation)
{
get();
} lock (addOperation)
{
create_add();
}

而我认为它的执行方式是,

lock (operation)
{
get();
create_add();
}

因此会出现我预期外的valueFactory函数执行多次的情况。微软MSDN中描述了一种更严重的情况:

  1. threadA 调用 GetOrAdd,未找到项,通过调用 valueFactory 委托创建要添加的新项。
  2. threadB 并发调用 GetOrAdd,其 valueFactory 委托受到调用,并且它在 threadA 之前到达内部锁,并将其新键值对添加到词典中。
  3. threadA 的用户委托完成,此线程到达锁位置,但现在发现已有项存在
  4. threadA 执行"Get",返回之前由 threadB 添加的数据。

因此,无法保证 GetOrAdd 返回的数据与线程的 valueFactory 创建的数据相同。 调用 AddOrUpdate 时可能发生相似的事件序列。

这个问题是非常隐蔽的,这个行为大部分的时候并不会造成问题,因为

  1. GetOrAdd同时执行的几率较小,valueFactory不会执行多遍
  2. 大部分的时候valueFactory是线程安全的,同时执行了多遍也看不出来

网上也有人讨论了这个问题:

对于valueFactory只允许执行一遍的场景,这两篇文章中也提到了同样的解决方法,那就是使用Lazy<Value>,相当于需要两次才执行实际的valueFactory函数。

这种方式下,第一次valueFactory虽然会执行多遍,但没有执行实际的创建操作,而在使用的时候Lazy<Value>使用的时候Lazy的原子性保证第二次valueFactory创建操作只会执行一次。

当然,也有更简单粗暴的做法,那就是对GetOrAdd和AddOrUpdate加锁,但那样的需要在所有调用的地方都加锁,实际实行起来很容易漏。

关于ConcurrentDictionary的线程安全的更多相关文章

  1. C# 中 ConcurrentDictionary 一定线程安全吗?

    根据 .NET 官方文档的定义:ConcurrentDictionary<TKey,TValue> Class 表示可由多个线程同时访问的线程安全的键/值对集合.这也是我们在并发任务中比较 ...

  2. .net framework 4 线程安全概述

    线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的.早期的时候, ...

  3. ConcurrentDictionary并发字典知多少?

    背景 在上一篇文章你真的了解字典吗?一文中我介绍了Hash Function和字典的工作的基本原理. 有网友在文章底部评论,说我的Remove和Add方法没有考虑线程安全问题. https://doc ...

  4. ConcurrentDictionary内部机制粗解

    ConcurrentDictionary是线程安全类,是什么在保证? 内部类 private class Tables { internal readonly Node[] m_buckets; // ...

  5. ABP使用及框架解析系列 - [Unit of Work part.2-框架实现]

    前言 ABP ABP是“ASP.NET Boilerplate Project”的简称. ABP的官方网站:http://www.aspnetboilerplate.com ABP在Github上的开 ...

  6. [Asp.net 5] Localization-简单易用的本地化-全球化信息

    本篇比较简单介绍Localization解决方案中: Microsoft.Framework.Globalization.CultureInfoCache 工程 CultureInfoGenerato ...

  7. C#学习笔记(一):一些零散但重要的知识点汇总

    集合类型 数组 数组需要注意的就是多维数组和数组的数组之间的区别,如下: using System; namespace Study { class Program { static void Mai ...

  8. Unit of Work

    ABP使用及框架解析系列 - [Unit of Work part.2-框架实现]   前言 ABP ABP是“ASP.NET Boilerplate Project”的简称. ABP的官方网站:ht ...

  9. C#集合类型大揭秘 【转载】

    [地址]https://www.cnblogs.com/songwenjie/p/9185790.html 集合是.NET FCL(Framework Class Library)的重要组成部分,我们 ...

随机推荐

  1. pt-table-checksum 3.0.4检测不出主从差异数据

    群里好几位同学问 pt-table-checksum 3.0.4, 主从两个表数据是不一致,为啥检测不出来?前段时间自己也测试过,只是没整理成随笔^_- 一.基本环境 VMware10.0+CentO ...

  2. Java内存模型-锁的内存语义

    一 引言 在说volatile的内存语义时,讲过这样一句话:想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.所 ...

  3. 分模块开发创建service子模块——(八)

    1.右击父工程新建maven子模块

  4. jdk1.8源码Thread与Runnable区别

    一.概念 Thread实现了Runnable接口 public class Thread implements Runnable { /* Make sure registerNatives is t ...

  5. RESTful 个人理解总结【转】

    转自:http://www.cnblogs.com/wang-yaz/p/9237981.html 一.什么是RESTful 面向资源 简单的说:RESTful是一种架构的规范与约束.原则,符合这种规 ...

  6. Shiro:授权控制

    对容易忽略的地方记录如下: 1.需要引入下面2个依赖,具体版本根据自身环境修改: <dependency> <groupId>org.apache.geronimo.bundl ...

  7. 洛谷P3379倍增LCA

    传送门 #include <iostream> #include <cstdio> #include <cstring> #include <cmath> ...

  8. (三)发布Dubbo服务

    我们现在来学习下发布Dubbo服务,主要参考dubbo开发包里的demo源码:由浅入深的讲解下这个小demo: github地址:https://github.com/apache/incubator ...

  9. 体会 git 之优越性

    既生瑜,何生亮.已有subversion,何需git?先有firefox叱咤一时,何需chrome来搅局? 原本以为之前的解决方案已经能够满足现时的需求,但这是真正的事实吗?直到新颖的工具降临,才惊叹 ...

  10. .NetCore 分页控件实现原理处理以及条件分页处理

    说明 自定义一个类继承TagHelper,注意自定义类的 必须以TagHelper结尾,这个有点类是属性 Attribute的写法 protected TagHelper(); // // 摘要: / ...