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. ASP.NET生成二维码

    下面使用ThoughtWorks.QRCode.dll这个类库,实现生成二维码 使用时需要增加:下面三个命名空间 using ThoughtWorks.QRCode.Codec; using Thou ...

  2. 在OS X 10.9配置WebDAV服务器联合NSURLSessionUploa…

    CHENYILONG Blog   在OS X 10.9配置WebDAV服务器联合NSURLSessionUploadTask实现文件上传iOS7推出的NSURLSession简化了NSURLConn ...

  3. UVALive 6467

    题目链接 : http://acm.sdibt.edu.cn/vjudge/contest/view.action?cid=2186#problem/C 题意:对于斐波那契数列,每个数都mod m , ...

  4. 用MFC(C++)实现拼音搜索

    2015年4月1日更新: 我在github开源了Objective-C版的拼音搜索项目,感兴趣的可以去看看: OC版拼音搜索 最近项目需要实现按照拼音搜索资源.在网上找了一下,这方面的东西太少了. J ...

  5. HDU 1501 Zipper 字符串

    题目大意:输入有一个T,表示有T组测试数据,然后输入三个字符串,问第三个字符串能否由第一个和第二个字符串拼接而来,拼接的规则是第一个和第二个字符串在新的字符串中的前后的相对的顺序不能改变,问第三个字符 ...

  6. aps.net webform框架下页面服务器端控件和html控件用法

    (1)select 下拉框 前端: <select name="gameserverlist" id="gameserverlist" runat=&qu ...

  7. B - C Looooops POJ - 2115 (扩展欧几里得)

    题目链接:https://cn.vjudge.net/contest/276376#problem/B 题目大意:for( int  i= A ; i != B; i+ = c ),然后给你A,B,C ...

  8. python3之memcached

    1.memcached介绍 Memcached是一个自由开源的,高性能,分布式内存对象缓存系统. Memcached是以LiveJournal旗下Danga Interactive公司的Brad Fi ...

  9. 如何将IOS版本的更新下载文件指向到自己的服务器

    针对那些使用企业签名但是没有发布到AppSotre的IOS版本APP自动更新问题解决方案: 在apicloud中是这样说明的: 因为要填写plist地址所以不能向安卓那样直接填写服务器文件地址,但是直 ...

  10. 如何计算 App 的启动时间

    应用启动场景 事实上 Android 中一个 App 的启动时间可以准确计算的.但是要分场景.也就是说要分开游戏和应用. 大家都知道,在Android中,游戏开发和应用开发是两码事.所以我们需要分开来 ...