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

2023-11-06 12:08

本文主要是介绍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

这篇关于ConcurrentDictionary线程不安全么,你难道没疑惑,你难道弄懂了么?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/356519

相关文章

客户案例:安全海外中继助力知名家电企业化解海外通邮困境

1、客户背景 广东格兰仕集团有限公司(以下简称“格兰仕”),成立于1978年,是中国家电行业的领军企业之一。作为全球最大的微波炉生产基地,格兰仕拥有多项国际领先的家电制造技术,连续多年位列中国家电出口前列。格兰仕不仅注重业务的全球拓展,更重视业务流程的高效与顺畅,以确保在国际舞台上的竞争力。 2、需求痛点 随着格兰仕全球化战略的深入实施,其海外业务快速增长,电子邮件成为了关键的沟通工具。

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

2024网安周今日开幕,亚信安全亮相30城

2024年国家网络安全宣传周今天在广州拉开帷幕。今年网安周继续以“网络安全为人民,网络安全靠人民”为主题。2024年国家网络安全宣传周涵盖了1场开幕式、1场高峰论坛、5个重要活动、15场分论坛/座谈会/闭门会、6个主题日活动和网络安全“六进”活动。亚信安全出席2024年国家网络安全宣传周开幕式和主论坛,并将通过线下宣讲、创意科普、成果展示等多种形式,让广大民众看得懂、记得住安全知识,同时还

【Kubernetes】K8s 的安全框架和用户认证

K8s 的安全框架和用户认证 1.Kubernetes 的安全框架1.1 认证:Authentication1.2 鉴权:Authorization1.3 准入控制:Admission Control 2.Kubernetes 的用户认证2.1 Kubernetes 的用户认证方式2.2 配置 Kubernetes 集群使用密码认证 Kubernetes 作为一个分布式的虚拟

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

java线程深度解析(二)——线程互斥技术与线程间通信

http://blog.csdn.net/daybreak1209/article/details/51307679      在java多线程——线程同步问题中,对于多线程下程序启动时出现的线程安全问题的背景和初步解决方案已经有了详细的介绍。本文将再度深入解析对线程代码块和方法的同步控制和多线程间通信的实例。 一、再现多线程下安全问题 先看开启两条线程,分别按序打印字符串的

java线程深度解析(一)——java new 接口?匿名内部类给你答案

http://blog.csdn.net/daybreak1209/article/details/51305477 一、内部类 1、内部类初识 一般,一个类里主要包含类的方法和属性,但在Java中还提出在类中继续定义类(内部类)的概念。 内部类的定义:类的内部定义类 先来看一个实例 [html]  view plain copy pu