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. JavaScript编写风格指南 (一)

    //参考<编写可维护的Javascript>  一:缩进// 第一行的层级由4个空格组成,避免使用制表符tab进行缩进 //好的写法if (true) {    doSomething() ...

  2. dialog 菜单

    dialog 菜单 # 默认将所有输出用 stderr 输出,不显示到屏幕 使用参数 --stdout 可将选择赋给变量 # 退出状态 0正确 1错误 窗体类型 --calendar # 日历 --c ...

  3. 第10月第20天 afnetwork like MKNetworkEngine http post

    1. + (AFHTTPRequestOperation *)requestSellerWithCompletion:(requestFinishedCompletionBlock)successBl ...

  4. ListView position

    在使用listview的时候,我们经常会在listview的监听事件中,例如OnItemClickListener(onItemClick)中,或listview的adapter中(getView.g ...

  5. Jquery ajax json 不执行success的原因 坑爹

    最近在看jQuery的API文档,在使用到jQuery的ajax时,如果指定了dataType为json,老是不执行success回调,而是执行了error回调函数,极度郁闷.后面改为1.2.6版本可 ...

  6. jmeter --使用put方法上传文件

    今天来记录下put上传文件遇到的坑吧!折腾死我了, 刚开始的时候用的jmeter3.0,各种尝试,发现始终告诉我文件内容为空<actual file content,not shown here ...

  7. 打开文件或者uri的方式--------进程启动文件和启动者启动文件

    The  Process class in  System.Diagnostics allows you to launch a new process.For security reasons, t ...

  8. java.io.StreamCorruptedException: invalid stream header: EFBFBDEF 问题解决

    错误方式 @Test public void testDeserializeTest() throws IOException, ClassNotFoundException { ByteArrayO ...

  9. js单元测试框架

    js单元测试框架 前端测试框架对比(js单元测试框架对比) 本文主要目的在于横评业界主流的几款前端框架,顺带说下相关的一些内容. 测试分类 通常应用会有 单元测试(Unit tests) 和 功能测试 ...

  10. Python3 简明教程学习(上)

    一.开始 Python 之旅交互模式 1.Ctrl + D 输入一个 EOF 字符来退出解释器,也可以键入 exit() 来退出 2.#!/usr/bin/env python3 中#!称为 Sheb ...