ConcurrentDictionary线程不安全么,你难道没疑惑,你难道弄懂了么?

事情不太多时,会时不时去看项目中同事写的代码可以作个参考或者学习,个人觉得只有这样才能走的更远,抱着一副老子天下第一的态度最终只能是井底之蛙。前两篇写到关于断点传续的文章,还有一篇还未写出,后续会补上,这里我们穿插一篇文章,这是我看到同事写的代码中有ConcurrentDictionary这个类,之前并未接触过,就深入了解了一下,所以算是查漏补缺,基础拾遗吧,想要学习的这种劲头越有,你会发觉突然涌现的知识越多,学无止境!。

话题

本节的内容算是非常老的一个知识点,在.NET4.0中就已经出现,并且在园中已有园友作出了一定分析,为何我又拿出来讲呢?理由如下:

(1)没用到过,算是自己的一次切身学习。

(2)对比一下园友所述,我想我是否能讲的更加详尽呢?挑战一下。

(3)是否能够让读者理解的更加透彻呢?打不打脸不要紧,重要的是学习的过程和心得。

在.NET1.0中出现了HashTable这个类,此类不是线程安全的,后来为了线程安全又有了Hashtable.Synchronized,之前看到同事用Hashtable.Synchronized来进行实体类与数据库中的表进行映射,紧接着又看到别的项目中有同事用ConcurrentDictionary类来进行映射,一查资料又发现Hashtable.Synchronized并不是真正的线程安全,至此才引起我的疑惑,于是决定一探究竟, 园中已有大篇文章说ConcurrentDictionary类不是线程安全的。为什么说是线程不安全的呢?至少我们首先得知道什么是线程安全,看看其定义是怎样的。定义如下:

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

一搜索线程安全比较统一的定义就是上述所给出的,园中大部分对于此类中的GetOrAdd或者AddOrUpdate参数含有委托的方法觉得是线程不安全的,我们上述也给出线程安全的定义,现在我们来看看其中之一。

        private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>();

public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("JeffckWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2);

PrintValue("JeffckyWang from cnblogs");
Console.ReadKey();
}

public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
return valueToPrint;
});
Console.WriteLine(valueFound);
}

对于GetOrAdd方法它是怎样知道数据应该是添加还是获取呢?该方法描述如下:

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

当给出指定键时,会去进行遍历若存在直接返回其值,若不存在此时会调用第二个参数也就是委托将运行,并将其添加到字典中,最终返回给调用者此键对应的值。

此时运行上述程序我们会得到如下二者之一的结果:

640?wx_fmt=png


我们开启两个线程,上述运行结果不都是一样的么, 按照上述定义应该是线程安全才对啊,好了到了这里关于线程安全的定义我们应该消除以下两点才算是真正的线程安全。

(1)竞争条件

(2)死锁

那么问题来了,什么又是竞争条件呢?好吧,我是传说中的十万个什么。

就像女朋友说的哪有这么多为什么,我说的都是对的,不要问为什么,但对于这么严谨的事情,我们得实事求是,是不。竞争条件是软件或者系统中的一种行为,它的输出不会受到其他事件的影响而影响,若因事件受到影响,如果事件未发生则后果很严重,继而产生bug诺。 最常见的场景发生在当有两个线程同时共享一个变量时,一个线程在读这个变量,而另外一个变量同时在写这个变量。比如定义一个变量初始化为0,现在有两个线程共享此变量,此时有一个线程操作将其增加1,同时另外一个线程操作也将其增加1此时此时得到的结果将是1,而实际上我们期待的结果应该是2,所以为了解决竞争我们通过用锁机制来实现在多线程环境下的线程安全。

那么问题来了,什么是死锁呢?

至于死锁则不用多讲,死锁发生在多线程或者并发环境下,为了等待其他操作完成,但是其他操作一直迟迟未完成从而造成死锁情况。满足什么条件才会引起死锁呢?如下:

(1)互斥:只有进程在给定的时间内使用资源。

(2)占用并等待。

(3)不可抢先。

(4)循环等待。

到了这里我们通过对线程安全的理解明白一般为了线程安全都会加锁来进行处理,而在ConcurrentDictionary中参数含有委托的方法并未加锁,但是结果依然是一样的,至于未加锁说是为了出现其他不可预料的情况,依据我个人理解并非完全线程不安全,只是对于多线程环境下有可能出现数据不一致的情况,为什么说数据不一致呢?我们继续向下探讨。我们将上述方法进行修改如下:

        public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
});
Console.WriteLine(valueFound);
}

主程序输出运行次数:

            var task1 = Task.Run(() => PrintValue("JeffckyWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2);

PrintValue("JeffckyWang from cnblogs");

Console.WriteLine(string.Format("运行次数为:{0}", _runCount));

640?wx_fmt=png

此时我们看到确确实实获得了相同的值,但是却运行了两次,为什么会运行两次,此时第二个线程在运行调用之前,而第一个线程的值还未进行保存而导致。整个情况大致可以进行如下描述:

(1)线程1调用GetOrAdd方法时,此键不存在,此时会调用valueFactory这个委托。

(2)线程2也调用GetOrAdd方法,此时线程1还未完成,此时也会调用valueFactory这个委托。

(3)线程1完成调用,并返回JeffckyWang值到字典中,此时检查键还并未有值,然后将其添加到新的KeyValuePair中,并将JeffckyWang返回给调用者。

(4)线程2完成调用,并返回cnblogs值到字典中,此时检查此键的值已经被保存在线程1中,于是中断添加其值用线程1中的值进行代替,最终返回给调用者。

(5)线程3调用GetOrAdd方法找到键key其值已经存在,并返回其值给调用者,不再调用valueFactory这个委托。

从这里我们知道了结果是一致的,但是运行了两次,其上是三个线程,若是更多线程,则会重复运行多次,如此或造成数据不一致,所以我的理解是并非完全线程不安全。难道此类中的两个方法是线程不安全,.NET团队没意识到么,其实早就意识到了,上述也说明了如果为了防止出现意想不到的情况才这样设计,说到这里就需要多说两句,开源最大的好处就是能集思广益,目前已开源的 Microsoft.AspNetCore.Mvc.Core ,我们可以查看中间件管道源代码如下:

    /// <summary>
/// Builds a middleware pipeline after receiving the pipeline from a pipeline provider
/// </summary>
public class MiddlewareFilterBuilder
{
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
// threads but only one of the objects succeeds in creating a pipeline.
private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache
= new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
private readonly MiddlewareFilterConfigurationProvider _configurationProvider;

public IApplicationBuilder ApplicationBuilder { get; set; }
}

通过ConcurrentDictionary类调用上述方法无法保证委托调用的次数,在对于mvc中间管道只能初始化一次所以ASP.NET Core团队使用Lazy<>来初始化,此时我们将上述也进行上述对应的修改,如下:

               private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
= new ConcurrentDictionary<string, Lazy<string>>();


var valueFound = _lazyDictionary.GetOrAdd("key",
x => new Lazy<string>(
() =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);

此时将得到如下:

640?wx_fmt=png

我们将第二个参数修改为Lazy<string>,最终调用valueFound.value将调用次数输出到控制台上。此时我们再来解释上述整个过程发生了什么。

(1)线程1调用GetOrAdd方法时,此键不存在,此时会调用valueFactory这个委托。

(2)线程2也调用GetOrAdd方法,此时线程1还未完成,此时也会调用valueFactory这个委托。

(3)线程1完成调用,返回一个未初始化的Lazy<string>对象,此时在Lazy<string>对象上的委托还未进行调用,此时检查未存在键key的值,于是将Lazy<striing>插入到字典中,并返回给调用者。

(4)线程2也完成调用,此时返回一个未初始化的Lazy<string>对象,在此之前检查到已存在键key的值通过线程1被保存到了字典中,所以会中断创建,于是其值会被线程1中的值所代替并返回给调用者。

(5)线程1调用Lazy<string>.Value,委托的调用以线程安全的方式运行,所以如果被两个线程同时调用则只运行一次。

(6)线程2调用Lazy<string>.Value,此时相同的Lazy<string>刚被线程1初始化过,此时则不会再进行第二次委托调用,如果线程1的委托初始化还未完成,此时线程2将被阻塞,直到完成为止,线程2才进行调用。

(7)线程3调用GetOrAdd方法,此时已存在键key则不再调用委托,直接返回键key保存的结果给调用者。

上述使用Lazy来强迫我们运行委托只运行一次,如果调用委托比较耗时此时不利用Lazy来实现那么将调用多次,结果可想而知,现在我们只需要运行一次,虽然二者结果是一样的。我们通过调用Lazy<string>.Value来促使委托以线程安全的方式运行,从而保证在某一个时刻只有一个线程在运行,其他调用Lazy<string>.Value将会被阻塞直到第一个调用执行完,其余的线程将使用相同的结果。

那么问题来了调用Lazy<>.Value为何是线程安全的呢? 

我们接下来看看Lazy对象。方便演示我们定义一个博客类

    public class Blog
{
public string BlogName { get; set; }

public Blog()
{
Console.WriteLine("博客构造函数被调用");
BlogName = "JeffckyWang";
}
}

接下来在控制台进行调用:

            var blog = new Lazy<Blog>();
Console.WriteLine("博客对象被定义");
if (!blog.IsValueCreated) Console.WriteLine("博客对象还未被初始化");
Console.WriteLine("博客名称为:" + (blog.Value as Blog).BlogName);
if (blog.IsValueCreated)
Console.WriteLine("博客对象现在已经被初始化完毕");

打印如下:

640?wx_fmt=png

通过上述打印我们知道当调用blog.Value时,此时博客对象才被创建并返回对象中的属性字段的值,上述布尔属性即IsValueCreated显示表明Lazy对象是否已经被初始化,上述初始化对象过程可以简述如下:

            var lazyBlog = new Lazy<Blog>
(
() =>
{
var blogObj = new Blog() { BlogName = "JeffckyWang" };
return blogObj;
}
);

打印结果和上述一致。上述运行都是在非线程安全的模式下进行,要是在多线程环境下对象只被创建一次我们需要用到如下构造函数:

 public Lazy(LazyThreadSafetyMode mode);
public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);

通过指定LazyThreadSafetyMode的枚举值来进行。

(1)None = 0【线程不安全】

(2)PublicationOnly = 1【针对于多线程,有多个线程运行初始化方法时,当第一个线程完成时其值则会设置到其他线程】

(3)ExecutionAndPublication = 2【针对单线程,加锁机制,每个初始化方法执行完毕,其值则相应的输出】

我们演示下情况:

    public class Blog
{
public int BlogId { get; set; }
public Blog()
{
Console.WriteLine("博客构造函数被调用");
}
}
        static void Run(object obj)
{
var blogLazy = obj as Lazy<Blog>;
var blog = blogLazy.Value as Blog;
blog.BlogId++;
Thread.Sleep(100);
Console.WriteLine("博客Id为:" + blog.BlogId);

}
            var lazyBlog = new Lazy<Blog>
(
() =>
{
var blogObj = new Blog() { BlogId = 100 };
return blogObj;
}, LazyThreadSafetyMode.PublicationOnly
);
Console.WriteLine("博客对象被定义");
ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);
ThreadPool.QueueUserWorkItem(new WaitCallback(Run), lazyBlog);

结果打印如下:

640?wx_fmt=png

奇怪的是当改变线程安全模式为 LazyThreadSafetyMode.ExecutionAndPublication 时结果应该为101和102才是,居然返回的都是102,但是将上述blog.BogId++和暂停时间顺序颠倒时如下:

  Thread.Sleep(100);          
blog.BlogId++;

此时两个模式返回的都是101和102,不知是何缘故!上述在ConcurrentDictionary类中为了两个方法能保证线程安全我们利用Lazy来实现,默认的模式为 LazyThreadSafetyMode.ExecutionAndPublication 保证委托只执行一次。为了不破坏原生调用ConcurrentDictionary的GetOrAdd方法,但是又为了保证线程安全,我们封装一个方法来方便进行调用。

        public class LazyConcurrentDictionary<TKey, TValue>
{
private readonly ConcurrentDictionary<TKey, Lazy<TValue>> concurrentDictionary;

public LazyConcurrentDictionary()
{
this.concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
}

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
var lazyResult = this.concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication));

return lazyResult.Value;
}
}

原封不动的进行方法调用:

        private static int _runCount = 0;
private static readonly LazyConcurrentDictionary<string, string> _lazyDictionary
= new LazyConcurrentDictionary<string, string>();

public static void Main(string[] args)
{

var task1 = Task.Run(() => PrintValue("JeffckyWang"));
var task2 = Task.Run(() => PrintValue("cnblogs"));
Task.WaitAll(task1, task2);

PrintValue("JeffckyWang from cnblogs");
Console.WriteLine(string.Format("运行次数为:{0}", _runCount));
Console.Read();
}

public static void PrintValue(string valueToPrint)
{
var valueFound = _lazyDictionary.GetOrAdd("key",
x => {
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
});
Console.WriteLine(valueFound);
}

最终正确打印只运行一次的结果,如下:

640?wx_fmt=png

总结

本节我们学习了ConcurrentDictionary类里面有两个方法严格来说非线程安全,但是也可以得到相同的结果,若我们仅仅只是得到相同的结果且操作不是太耗时其实完全可以忽略这一点,若当利用ConcurrentDictionary类中的此二者方法来做比较耗时的操作,此时就要注意让其线程安全利用Lazy来保证其只能执行一次,所以对ConcurrentDictionary来说并非所有情况都要实现严格意义上的线程安全,根据实际场景而定才是最佳解决方案。时不时多看看别人写的代码,涨涨见识,每天积累一点,日子长了就牛逼了!

原文地址:https://www.cnblogs.com/ywsoftware/p/10888798.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 
640?wx_fmt=jpeg

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

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

相关文章

记一次ORM的权衡和取舍

面对ORM的选型&#xff0c;有些人是根据自己熟悉程度来评判&#xff0c;有些人是根据他人的推荐来抉择&#xff0c;有些人觉得都差不多&#xff0c;随便了。当自己要真正做选择的时候&#xff0c;以上的这些依据都无法真正说服自己&#xff0c;因为不同的业务需求&#xff0c;不…

出让执行权:Task.Yield, Dispatcher.Yield

一个耗时的任务&#xff0c;可以通过 Task.Yield 或者 Dispatcher.Yield 来中断以便分割成多个小的任务片段执行。Yield 这个词很有意思&#xff0c;叫做“屈服”“放弃”“让步”&#xff0c;字面意义上是让出当前任务的执行权&#xff0c;转而让其他任务可以插入执行。Task、…

VS Code 即将迎来再一次的 logo 更新!已可在 Insiders 版本尝鲜

为什么要说“再一次”&#xff1f; 相信 VS Code 的老用户都还记得两年前的 logo 更新风波吧。当时 VS Code 改了新 logo 之后&#xff0c;VS Code 的用户们一片哀嚎&#xff0c;纷纷觉得新 logo 太丑&#xff0c;在 GitHub 和各种社交媒体上各种吐槽&#xff01;不过幸运的是&…

从零开始在 Windows 上部署 .NET Core 到 Kubernetes

本章节所有代码已上传至&#xff1a;https://github.com/Seanwong933/.NET-Core-on-Kubernetes文末附有本人遇到过的 Docker 和 k8s 的故障排除。本文目标&#xff1a;带领大家在 Kubernetes 上部署一个 .NET Core Api 的单节点集群。后续文章会帮助大家继续深入。安装 Kuberne…

.NET Core微服务 权限系统+工作流(一)权限系统

一、前言实际上权限系统老早之前我就在一直开发&#xff0c;大概在刚毕业没多久就想一个人写一个系统&#xff0c;断断续续一直坚持到现在&#xff0c;毕竟自己亲动手自写的系统才有收获&#xff0c;本篇仅介绍权限。小小系统上不了台面&#xff0c;望各位大神勿喷。二、系统介…

iNeuOS云操作系统,.NET Core全系打造

一.演示地址演示地址&#xff1a; 进入iNeuOS系统。&#xff08;建议使用chrome浏览器&#xff09;http://192.144.173.38:8081/login.html测试名称&#xff1a;admin测试密码&#xff1a;admin下载《iNeuOS云操作系统演示应用手册》 链接&#xff1a;https://pan.baidu.co…

译 | 你到底有多精通 C# ?

点击上方蓝字关注“汪宇杰博客”文&#xff1a;Damir Arh译&#xff1a;Edi Wang即使是具有良好 C# 技能的开发人员有时候也会编写可能会出现意外行为的代码。本文介绍了属于该类别的几个 C# 代码片段&#xff0c;并解释了令人惊讶的行为背后的原因。Null 值我们都知道&#xf…

各大主流K8S服务全方位能力比对

大家好&#xff0c;趁打开流量主的东风&#xff0c;特此贡献一篇长文&#xff0c;分析一下目前国内国外几大著名云厂商的kubernetes服务&#xff0c;以飨诸君。文起之前&#xff0c;先聊态度。 我本人是十分看好k8s的发展的&#xff0c;为何&#xff1f; 理因古往今来&#xff…

.NET Core 的Generic Host 之Generic Host Builder

通用Host(Generic Host) 与 web Host 不同的地方就是通用Host解耦了Http请求管道&#xff0c;使得通用Host拥有更广的应用场景。比如&#xff1a;消息收发、后台任务以及其他非http的工作负载。这些场景都可以通过使用通用Host拥有横切&#xff08;Cross-cutting&#xff09;的…

.NET Core微服务 权限系统+工作流(二)工作流系统

一、前言接上一篇 .NET Core微服务 权限系统工作流&#xff08;一&#xff09;权限系统 &#xff0c;再来一发工作流&#xff0c;我在接触这块开发的时候一直好奇它的实现方式&#xff0c;翻看各种工作流引擎代码&#xff0c;探究其实现方式&#xff0c;个人总结出来一个核心要…

开源分布式Job系统,调度与业务分离-如何创建一个计划HttpJob任务

项目介绍&#xff1a;Hangfire&#xff1a;是一个开源的job调度系统,支持分布式JOB&#xff01;&#xff01;Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的。可以独立更新Hangfire版本不影响&#xff01;该组件已被Hangfire官方采纳&#xff…

Angular 8正式发布!

Angular 团队今天宣布推出 Angular 8 正式版。作为一个期待已久的重大版本更新&#xff0c;Angular 8 为框架、Angular Material 和命令行界面工具 Angular CLI 带来了大量的改进和新功能。团队表示 Angular 8 显著减少了在现代浏览器中应用程序的启动时间、提供了用于定制 CLI…

.NET Core 3.0 webapi集成Swagger 5.0

在项目中引用Swashbuckle.AspNetCore和Swashbuckle.AspNetCore.Filters两个dll&#xff0c;在Startup中的ConfigureServices相关配置代码如下 两个重点&#xff1a;1、options.DocumentFilter<HiddenApiFilter>();定义那些接口方法被隐藏2、启用oauth2安全授权访问…

站点部署,IIS配置优化指南

通常把站点发布到IIS上运行正常后&#xff0c;很少会去考虑IIS提供的各种参数&#xff0c;如何配置才是最适合当前站点运行需要的&#xff1f;这篇文章&#xff0c;从基本设置、回收机制、性能、并发、安全性等IIS设置讲解应当如何优化。先来“IIS应用程序池”优化后的参数配置…

张高兴的.NET Core IoT 入门指南:(四)使用 SPI 进行通信

什么是 SPI和上一篇文章的 I2C 总线一样&#xff0c;SPI&#xff08;Serial Peripheral Interface&#xff0c;串行外设接口&#xff09;也是设备与设备间通信方式的一种。SPI 是一种全双工&#xff08;数据可以两个方向同时传输&#xff09;的串行通信总线&#xff0c;由摩托罗…

ASP.NET Core 中使用IHttpClientFactory发出HTTP请求

1.HttpClient类使用存在的问题HttpClient类的使用所存在的问题&#xff0c;百度搜索的文章一大堆&#xff0c;好多都是单纯文字描述&#xff0c;让人感觉不太好理解&#xff0c;为了更好理解HttpClient使用存在的问题&#xff0c;下面让我们通过代码跟示例来描述。using(var cl…

linux 安装 powershell

linux 安装 powershellIntropowershell 已经推出了一个 Powershell Core&#xff0c; 版本号对应 Powershell 6.x&#xff0c;可以跨平台&#xff0c;支持 Linux 和 mac. 这使得对于熟练使用 Powershell 进行开发运维的一些开发者来说无疑是个福音。PowerShell 和 PowerShell C…

精彩回放 | 玩转 VS Code 物联网开发

"Visual Studio Code&#xff1a;物联网开发利器"技术分享圆满落下帷幕&#xff01;感谢韩老师的粉丝们&#xff01;感谢热情的观众朋友们&#xff01;点击文末阅读原文&#xff0c;可以观看视频回放~这几年物联网越来越火&#xff0c;大家都在说物联网&#xff0c;那…

重磅!开放EasyCharts插件源代码!

开源代码地址https://github.com/EasyChart/EasyCharts前 言不知不觉&#xff0c;Excel图表插件EasyCharts已经面世两年啦&#xff0c;今天突然发现百度网盘中的下载次数居然达到近4万&#xff0c;在这里非常感谢大家对EasyCharts的厚爱。由于工作太忙&#xff0c;时间有限&a…

构建可读性更高的 ASP.NET Core 路由

一、前言不知你在平时上网时有没有注意到&#xff0c;绝大多数网站的 URL 地址都是小写的英文字母&#xff0c;而我们使用 .NET/.NET Core MVC 开发的项目&#xff0c;因为在 C# 中类和方法名采用的是 Pascal 命名规范&#xff0c;根据 .NET 框架默认的路由规则&#xff0c;项目…