好久不见,马甲哥封闭居家半个月,记录之前遇到的一件小事。

ConcurrentDictionary<TKey,TValue>绝大部分api都是线程安全且原子性的

唯二的例外是接收工厂委托的api:AddOrUpdateGetOrAdd这两个api不是原子性的,需要引起重视。

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.

之前有个同事就因为这个case背了一个P。

AddOrUpdate(TKey, TValue, Func<TKey,TValue,TValue> valueFactory);

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

(注意,包括其他接收工厂委托的重载函数)

Q1: valueFactory工厂函数不在锁定范围,为什么不在锁范围?

A: 还不是因为微软不相信你能写出健壮的业务代码,未知的业务代码可能造成死锁。

However, delegates for these methods are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, the code executed by these delegates is not subject to the atomicity of the operation.

Q2:带来的效果?

  • valueFactory工厂函数可能会多次执行
  • 虽然会多次执行, 但插入的值永远是一个,插入的值取决于哪个线程率先插入字典。

Q3: 怎么做到的?

A: 源代码做了double check了,后续线程通过工厂类创建值后,会再次检查字典,发现已有值,会丢弃自己创建的值。

示例代码:

using System.Collections.Concurrent;

public class Program
{
private static int _runCount = 0;
private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>(); public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("The first value"));
var task2 = Task.Run(() => PrintValue("The second value"));
var task3 = Task.Run(() => PrintValue("The three value"));
var task4 = Task.Run(() => PrintValue("The four value"));
Task.WaitAll(task1, task2, task4,task4); PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount}");
} public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
});
Console.WriteLine(valueFound);
}
}

上面4个线程并发插入字典,每次随机输出,_runCount=4显示工厂类执行4次。

Q4:如果工厂产值的代价很大,不允许多次创建,如何实现?

笔者的同事之前就遇到这样的问题,高并发请求频繁创建redis连接,直接打挂了机器。

A: 有一个trick能解决这个问题: valueFactory工厂函数返回Lazy容器.

using System.Collections.Concurrent;

public class Program
{
private static int _runCount2 = 0;
private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
= new ConcurrentDictionary<string, Lazy<string>>(); public static void Main(string[] args)
{
task1 = Task.Run(() => PrintValueLazy("The first value"));
task2 = Task.Run(() => PrintValueLazy("The second value"));
task3 = Task.Run(() => PrintValueLazy("The three value"));
task4 = Task.Run(() => PrintValueLazy("The four value"));
Task.WaitAll(task1, task2, task4, task4); PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount2}");
} public static void PrintValueLazy(string valueToPrint)
{
var valueFound = _lazyDictionary.GetOrAdd("key",
x => new Lazy<string>(
() =>
{
Interlocked.Increment(ref _runCount2);
Thread.Sleep(100);
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);
}
}



上面示例,依旧会稳定随机输出,但是_runOut=1表明产值动作只执行了一次、

valueFactory工厂函数返回Lazy容器是一个精妙的trick。

① 工厂函数依旧没进入锁定过程,会多次执行;

② 与最上面的例子类似,只会插入一个Lazy容器(后续线程依旧做double check发现字典key已经有Lazy容器了,会放弃插入);

③ 线程执行Lazy.Value, 这时才会执行创建value的工厂函数;

④ 多个线程尝试执行Lazy.Value, 但这个延迟初始化方式被设置为ExecutionAndPublication

不仅以线程安全的方式执行, 而且确保只会执行一次构造函数。

public Lazy(Func<T> valueFactory)
:this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false)
{
}
控制构造函数执行的枚举值 描述
ExecutionAndPublication 能确保只有一个线程能够以线程安全方式执行构造函数
None 线程不安全
Publication 并发线程都会执行初始化函数,以先完成初始化的值为准

IHttpClientFactory在构建<命名HttpClient,活跃连接Handler>字典时, 也用到了这个技巧,大家自行欣赏DefaultHttpCLientFactory源码


总结

为解决ConcurrentDictionary GetOrAdd(key, valueFactory) 工厂函数在并发场景下被多次执行的问题。

① valueFactory工厂函数产生Lazy容器

② 将Lazy容器的值初始化姿势设定为ExecutionAndPublication(线程安全且执行一次)。

两姿势缺一不可。

ConcurrentDictionary<T,V> 的这两个操作不是原子性的的更多相关文章

  1. dpkg: error: -i (--install) 和 -i (--install) 两个操作之间有矛盾

    1 错误描述 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ sudo dpkg -i -i WineQQ2013-20131120-Longene.deb [ ...

  2. 【spring data jpa】使用spring data jpa时,关于service层一个方法中进行【删除】和【插入】两种操作在同一个事务内处理

    场景: 现在有这么一个情况,就是在service中提供的一个方法是先将符合条件的数据全部删除,然后再将新的条件全部插入数据库中 这个场景需要保证service中执行两步 1.删除 2.插入 这两步自然 ...

  3. MySQL Index--NOT IN和不等于两类操作无法走索引?

    经常被问,NOT IN和<>操作就无法走索引? 真想只有一个:具体问题具体分析,没有前提的问题都是耍流氓. 准备测试数据: ## 删除测试表 DROP TABLE IF EXISTS tb ...

  4. sk_buff整理笔记(两、操作函数)

    承接上一:sk_buff 整理笔记(一.数据结构)这一篇要讲的是内核为sk_buff结构提供的一些操作函数. 第一.首先要讲的是sk_buff中的四大指针: 四大指针各自是:head.data.tai ...

  5. [转载]redis持久化的两种操作RDB和AOF

    Redis 持久化: 提供了多种不同级别的持久化方式:一种是RDB,另一种是AOF. RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot). AO ...

  6. selectAll, unSelectAll两个操作的实现

    private void updateBatchSelectionStatus() {     ContactListAdapter.ViewHolder viewHolder = null;     ...

  7. std::vector 两种操作的比较

    swap assign 这里只想说明这三种操作的用处和效率.swap和assign都可以用在将一个vector的内容全部复制给另外一个vector,区别是swap会改变源vector,而assign会 ...

  8. Hive的两种操作模式

    Hive的客户端操作 Hive的客户端操作 通过JDBC操作Hive 通过Thrift操作Hive 通过JDBC操作Hive 首先 Hive 启动远程服务 hive --service hiveser ...

  9. 栈(Stack)和队列(Queue)是两种操作受限的线性表。

    (线性表:线性表是一种线性结构,它是一个含有n≥0个结点的有限序列,同一个线性表中的数据元素数据类型相同并且满足"一对一"的逻辑关系. "一对一"的逻辑关系指的 ...

随机推荐

  1. c# 国内外ORM 框架 dapper efcore sqlsugar freesql hisql sqlserver数据常规插入测试性能对比

    c# 国内外ORM 框架 dapper efcore sqlsugar freesql hisql sqlserver数据常规插入测试性能对比对比 在6.22 号发布了 c# sqlsugar,his ...

  2. 【小程序自动化Minium】二、元素定位-Page接口中的 get_element() 与 get_elements()

    UI自动化中的重要工作就是元素定位了,高效精准的定位方法可以让工作事半功倍. 在过去的一段web自动化经历中,使用的selenium库支持了多种定位方法,我们可以利用这些定位方法来做进一步封装,写出符 ...

  3. [ARC087D] Squirrel Migration 补题记录

    题目链接 简要题意: 给你一个\(N\)个节点的树,求一个\(1\cdots N\)的排列\((p_1,p_2,\cdots p_N)\) ,使得\(\sum dist(i,p_i)\)最大. 求这样 ...

  4. Identity Server 4资源拥有者密码认证控制访问API

    基于上一篇文章中的代码进行继续延伸,只需要小小的改动即可,不明白的地方可以先看看本人上一篇文章及源码: Identity Server 4客户端认证控制访问API 一.QuickStartIdenti ...

  5. JAVA解压.Z及.ZIP文件

    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-compress --> <dependency ...

  6. ArrayDeque(JDK双端队列)源码深度剖析

    ArrayDeque(JDK双端队列)源码深度剖析 前言 在本篇文章当中主要跟大家介绍JDK给我们提供的一种用数组实现的双端队列,在之前的文章LinkedList源码剖析当中我们已经介绍了一种双端队列 ...

  7. Unity-2D像素晶格化消融

    效果展示: ShaderLab Shader功能:图像变白+根据顶点的y值作透明裁剪: 才是可操作属性: IsDead: 控制像素变白,片元着色阶段IsDead小于0将颜色改为白色: Percent: ...

  8. WPS衍生新软件,填补一大缺憾,让office汗颜,Excel用户很开心

    首先,向大家报告一个好消息 WPS今天宣布品牌升级,有了更明确的品牌定位:专注创新的国民办公软件.WPS已有34年的历史了,自1988年诞生之日起,它在求伯君和雷军的带领下,一路披荆斩棘,虽有坎坷,但 ...

  9. postgres备份与恢复

    一  利用pgAdmin工具进行备份与恢复 pgAdmin是postgres官方的客户端管理工具,可以进行数据库的涉及与维护. 示例:以logs库为例记录操作       1)  右击需要备份的数据( ...

  10. ACWing93.递归实现组合型枚举

    题面 \93. 递归实现组合型枚举 从 1∼n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案. 输入格式 两个整数 n,m ,在同一行用空格隔开. 输出格式 按照从小到大的顺序输出所有方案 ...