ConcurrentDictionary字典操作竟然不全是线程安全的?

53f8f64f2009355d5ecbab497277dae8.gif

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

ConcurrentDictionary<TKey,TValue>绝大部分api都是线程安全的[1]
唯二的例外是接收工厂函数的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);
(注意,包括其他接收工厂委托的重载函数)

整个过程中涉及与字典直接交互的都用到到精细锁,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[2]了,后续线程通过工厂类创建值后,会再次检查字典,发现已有值,会丢弃自己创建的值。

示例代码:

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次。

e9c151a2b82e42bbb2fdb1b6f43e66d7.png

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);}
}
12638e95923123709dec40534fbbeb4a.png

上面示例,依旧会随机稳定输出,但是_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[3]能确保只有一个线程能够以线程安全方式执行构造函数
None线程不安全
Publication并发线程都会执行初始化函数,以先完成初始化的值为准

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

  • • https://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/


总结

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

① valueFactory工厂函数产生Lazy容器;

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

两姿势缺一不可。

本人会不时修正理解、更正错误,请适时移步左下角永久更新地址;也请看客大胆斧正。

引用链接

[1] ConcurrentDictionary<TKey,TValue>绝大部分api都是线程安全的: https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2?view=net-6.0
[2] double check: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L1152
[3] ExecutionAndPublication: https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-6.0#system-threading-lazythreadsafetymode-executionandpublication
[4] DefaultHttpCLientFactory源码: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs#L118

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282746.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

码农小汪-Hibernate学习8-hibernate关联关系注解表示@OneToMany mappedBy @ManyToMany @JoinTable...

近期我也是有点郁闷&#xff0c;究竟是程序中处理关联关系。还是直接使用外键处理关联关系呢&#xff1f;这个的说法不一致&#xff01;程序中处理这样的关联关系的话。自己去维护这样的约束。这样的非常乐观的一种做法&#xff01;或者是直接在数据库中处理这样的直接的外键关…

HTML中弹窗中加入图片,javascript里怎么实现点击图片弹出对话框?

JavaScript中可以使用document.getElementsByTagName方法后去img标签&#xff0c;然后遍历所有img标签并为其添加点击事件实现点击弹出对话框。JavaScript实现点击图片弹出对话框&#xff1a;img {width: 500px;height: 300px;}//获取所有的img标签var imgObjs document.getEl…

Java学习优秀网站

各类程序员学习路线图&#xff1a; http://www.runoob.com/coder-learn-path 博学谷&#xff1a; http://v.itcast.cn/map/22.html 慕课网&#xff1a; http://www.imooc.com/course/programdetail/pid/31 转载于:https://www.cnblogs.com/Arsene/p/6441831.html

Dcloud课程2 什么是Dcloud

Dcloud课程2 什么是Dcloud 一、总结 一句话总结&#xff1a;DCloud提供了一套快速开发应用的跨平台技术方案。 1、DCloud的产品架构&#xff1f; MUI(H5)HBuilder 2、什么是MUI&#xff1f; 最接近原生体验的移动App的UI框架。 3、什么是H5&#xff1f; html5功能增强标准 二、…

html5 轮询自动刷新数据,后台调用exe,前端定时轮询调用结果

前提使用asp.net core 2.1前端使用vueui使用element-ui前端发送请求用Axios新建asp.net core程序1.jpg修改Index.html{Layout null;}test{{ msg }}发送请求打开记事本// 创建 Vue 实例&#xff0c;得到 ViewModelvar vm new Vue({el: #app,data: {msg: 准备发送请求打开exe},…

洛谷 P2951 [USACO09OPEN]捉迷藏Hide and Seek

题目描述 Bessie is playing hide and seek (a game in which a number of players hide and a single player (the seeker) attempts to find them after which various penalties and rewards are assessed; much fun usually ensues). She is trying to figure out in which…

使用ssh免密码登录Linux服务器

频繁登录Linux服务器时&#xff0c;使用ssh <username><host>的方式登录&#xff0c;但是每次都需要输入密码是件很麻烦的事。我们还可以使用私钥/公钥对的方式在免密码登录服务器。首先需要在远程服务器中安装ssh-server服务&#xff0c;才可以使用ssh登录。如果没…

linux下tomcat开启远程调试

1.center下&#xff0c;在startup.sh文件首行中添加如下语句 declare -x CATALINA_OPTS"-server -Xdebug -Xnoagent -Djava.compilerNONE -Xrunjdwp:transportdt_socket,servery,suspendn,address8000"(不要换行&#xff0c;要在同一行)Ubuntu下&#xff0c;在catali…

.NET 7 RC1 发布

原文链接&#xff1a;https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-rc-1/[1]原文作者&#xff1a;Jeremy Likness&#xff0c;Angelos Petropoulos&#xff0c;Jon Douglas翻译&#xff1a;沙漠尽头的狼(谷歌翻译加持)今天我们宣布 .NET 7 候选版本 1。这是生产…

html 字符串最后加空格,html space空格符

htmlcss 代码在网页中如何插入打出空格字符实现方法**摘要浏览器总是会截短 HTML 页面中的空格。HTML将所有空格字符&#xff0c;制表符&#xff0c;空格和回车符压缩为一个字符。如果要缩进段落&#xff0c;则不能简单地键入五个空格然后开始文本。 如果您在文本中写 10 个空格…

.NET MAUI实战 FilePicker

1.概要最近在迁移 GeneralUpdate.Tool的时候需要用到文件选择&#xff0c;在MAUI中可以使用FilePicker进行选择。ref1: https://gitee.com/Juster-zhu/GeneralUpdateref2:https://docs.microsoft.com/zh-cn/dotnet/maui/platform-integration/storage/file-picker?tabswindows…

SQL Server中,with as使用介绍

一&#xff0e;WITH AS的含义 WITH AS短语&#xff0c;也叫做子查询部分&#xff08;subquery factoring&#xff09;&#xff0c;可以让你做很多事情&#xff0c;定义一个SQL片断&#xff0c;该SQL片断会被整个SQL语句所用到。有的时候&#xff0c;是为了让SQL语句的可读…

从新手机到老股票 闲鱼为何会沦为骗子与营销的新平台?

国内电商一直空缺一个有规模的综合二手交易平台。闲鱼的出现&#xff0c;有一定程度上满足了喜欢淘二手、喜欢“捡漏”的用户需求。虽加入了担保和第三方支付等环节&#xff0c;但这种随机的二手交易行为不可避免地会出现上当、受骗的情况出现。本质上来说&#xff0c;闲鱼仍然…

网上书店模板asp与html,一个简单的网上书城的例子(三)_asp实例

buy.asp:显示商品和用户购物&#xff01;DbPath SERVER.MapPath("ShopBag.mdb")Set conn Server.CreateObject("ADODB.Connection")conn.open "driver{Microsoft Access Driver (*.mdb)};dbq" & DbPathCategoryIDRequest("CategoryID…

使用C#编写一个.NET分析器(一)

译者注这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一&#xff0c;在国内只有很少很少的人了解和研究.NET分析器&#xff0c;它常被用于APM&#xff08;应用性能诊断&#xff09;、IDE、诊断工具中&#xff0c;比如Datadog的APM&#xff0c;Visual…

内置数据类型

Java语言提供了八种基本类型。六种数字类型&#xff08;四个整数型&#xff0c;两个浮点型&#xff09;&#xff0c;一种字符类型&#xff0c;还有一种布尔型。 byte&#xff1a; byte 数据类型是8位、有符号的&#xff0c;以二进制补码表示的整数&#xff1b; 最小值是 -128&…

算法学习之循环结构程序设计

for循环 打印1,2,3&#xff0c;...&#xff0c;n每个占一行。 #include <conio.h> #include<stdio.h> int main(){int i,n;scanf("%d",&n);for(i1;i<n;i){printf("%d\n",i);}getch();return 0; } 分支结合循环&#xff0c;威力很强大 输…

Linux常用命令 (分门别类)

一、系统安全: su: 用于切换当前用户身份到其他用户身份&#xff0c;变更时须输入所要变更的用户帐号与密码 sudo: 用来以其他身份来执行命令&#xff0c;预设的身份为root lastlog: 用于显示系统中所有用户最近一次登录信息 lastb: 用于显示用户错误的登录列表&#x…

hibernate自定义校验器使用(字段在in范围之内)

2019独角兽企业重金招聘Python工程师标准>>> 1.自定义注解类DigitsMustIn Constraint(validatedBy DigitsMustInValidator.class) //具体的实现 Target({java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.FIELD}) Retention(java.lang.a…

sql将html转成excel,使用SQL*PLUS,构建完美excel或html输出

通过SQL*PLUS我们可以构建友好的输出&#xff0c;满足多样化用户需求。本例通过简单示例&#xff0c;介绍通过sql*plus输出xls&#xff0c;html两种格式文件.首先创建两个脚本:1.main.sql用以设置环境&#xff0c;调用具体功能脚本2.功能脚本-get_tables.sql为实现具体功能之脚…