Add时出错

错误信息:
Index was outside the bounds of the array.
详细信息:
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at ****.GetEnumDescription(Enum value)
at ****.Page_Load(Object sender, EventArgs e)
at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e)
at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e)
at System.Web.UI.Control.OnLoad(EventArgs e)
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

出错的地方应该就是enumCache.Add(value, str);这句了。

但是左看右看,我也没看出这句有什么问题。这个方法里通过反射的方法将Enum里的Description元数据取出返回,但是由于反射是一个比较耗时的操作,所以这里用了一个Dictionary的对象将数据做了缓存。如果缓存里有就直接取缓存里的数据,如果没有再用常规方法获取。 www.it165.net

毫无头绪啊。

在Google上搜了一通,渐渐把问题聚焦在Dictionary.Insert方法里了,到MSDN里一查,果不其然是有问题的:

1.A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.
2. 
3.For a thread-safe alternative, see ConcurrentDictionary.

长期以来虽然知道Dictionary、List的实例是对象来着,但是用的时候都是当作值类型来用的,也从来没有考虑过在多线程环境下会有什么样的情况。但是这样就引来了一个问题,为什么多线程同时操作Dictionary对象的时候会出错呢?

其实我们平时使用Dictionary无非就用Add、Remove这样的方法,根本没有考虑过内部实现的机制。在Dictionary内部为了维 护Dictionary的功能和高效的特性,有自己的一些计数器和状态维护机制。Dictionary.Add方法实际上里头只有一句 话:this.Insert(key, value, true);也就是最终的实现都是在Insert方法里的。再用Reflector扒开Insert方法里的内容看看:


01.private void Insert(TKey key, TValue value, bool add)
02.{
03.int freeList;
04.if (key == null)
05.{
06.ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
07.}
08.if (this.buckets == null)
09.{
10.this.Initialize(0);
11.}
12.int num = this.comparer.GetHashCode(key) & 0x7fffffff;
13.int index = num % this.buckets.Length;
14.for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
15.{
16.if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
17.{
18.if (add)
19.{
20.ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
21.}
22.this.entries[i].value = value;
23.this.version++;
24.return;
25.}
26.}
27.if (this.freeCount > 0)
28.{
29.freeList = this.freeList;
30.this.freeList = this.entries[freeList].next;
31.this.freeCount--;
32.}
33.else
34.{
35.if (this.count == this.entries.Length)
36.{
37.this.Resize();
38.index = num % this.buckets.Length;
39.}
40.freeList = this.count;
41.this.count++;
42.}
43.this.entries[freeList].hashCode = num;
44.this.entries[freeList].next = this.buckets[index];
45.this.entries[freeList].key = key;
46.this.entries[freeList].value = value;
47.this.buckets[index] = freeList;
48.this.version++;
49.}

在这里可以看到有大量的计数器存在,而我们再来倒回头看看最开始抛出的异常对象:IndexOutOfRangeException,如果计数器出错,相当有可能在使用计数器做下标时出现下标越界的情况。那么这是.Net的Bug么?

在上面引用MSDN的时候微软已经明确说了,在多线程访问的时候不要使用Dictionary而应该使用ConCurrentDictionay,利用ConCurrentDictionay里的TryAdd、TryUpdate方法来避免出现类似的错误。

其实多线程并发计算的时候,经常会出现计数错误的情况。

举个例子,有这样一段程序:


01.int i = 0;
02.new Thread(() =>
03.{
04.for (int k = 0; k < 10; k++)
05.{
06.i++;
07.}
08.}).Start();
09. 
10.new Thread(() =>
11.{
12.for (int k = 0; k < 10; k++)
13.{
14.i++;
15.}
16.}).Start();

按正常情况来看,在这两个线程都执行完以后,i的值应该都是20,但是现实情况却是有一定概率i值会不等于20。
i++在执行的时候,CPU会得到类似这样的指令:
A: 表示这段指令是A线程上的,B: 表示这段指令是B线程上的


1.A: mov eax,[x]
2.A: inc eax
3.A: mov [x],eax

如果是在一个线程里顺序执行两次i++,那么执行的时候CPU得到的指令应该是这样的:


1.A: mov eax,[x]
2.A: inc eax
3.A: mov [x],eax
4.A: mov eax,[x]
5.A: inc eax
6.A: mov [x],eax

但是在两个线程中分别执行i++,情况就会变得非常复杂,CPU可能得到这样的指令:


1.A: mov eax,[x]
2.B: mov eax,[x]
3.A: inc eax
4.B: inc eax
5.A: mov [x],eax
6.B: mov [x],eax

假如在执行前x里的值是0,那么执行完以后线程A里的x的值变成了1,线程B里x的值也变成了1。也就是说,线程A和线程B里分别执行完i++以后,i实际上只增加了1(两个线程的eax是独立的)。

在分析完了原因以后,大致就可以知道如何解决这种问题了。

  1. 使用线程锁,在读写对象时将对象锁定直至操作结束(Link);
  2. 使用线程安全的ConcurrentDictionary对象,并使用TryAdd或TryUpdate方法操作(Link);
  3. 丢弃原有的Dictionary对象,重新创建一个新的对象,然后由GC将原先有错误的Dictionary对象回收。

经过昨天这个事情以后再也不能对线程掉以轻心。线程是好用,但是要用好还是要花费一番心思的。

Dictionary在多线程情况下的更多相关文章

  1. Java之HashMap在多线程情况下导致死循环的问题

    PS:不得不说Java编程思想这本书是真心强大.. 学习内容: 1.HashMap<K,V>在多线程的情况下出现的死循环现象   当初学Java的时候只是知道HashMap<K,V& ...

  2. 多线程情况下HashMap死循环的问题

    1.多线程put操作后,get操作导致死循环. 2.多线程put非null元素后,get操作得到null值. 3.多线程put操作,导致元素丢失. 死循环场景重现 下面我用一段简单的DEMO模拟Has ...

  3. Java面试题之在多线程情况下,单例模式中懒汉和饿汉会有什么问题呢?

    懒汉模式和饿汉模式: public class Demo { //private static Single single = new Single();//饿汉模式 private static S ...

  4. 关于多线程情况下Net-SNMP v3 版本导致进程假死情况的跟踪与分析

    1.问题描述 在使用net-snmp对交换机进行扫描的时候经常会出现进程假死的情况(就是进程并没有死掉,但是看不到它与外界进行任何的数据交互).这时候不知道进程内部发生了什么,虽然有日志信息,但进程已 ...

  5. Singleton多种实现方式的在多线程情况下的优缺点

    一.饿汉式 缺点:不能懒加载 // 不能懒加载 public class SingletonObject1 { private static final SingletonObject1 instan ...

  6. C#多线程环境下调用 HttpWebRequest 并发连接限制

    C#多线程环境下调用 HttpWebRequest 并发连接限制 .net 的 HttpWebRequest 或者 WebClient 在多线程情况下存在并发连接限制,这个限制在桌面操作系统如 win ...

  7. 在多线程环境下使用HttpWebRequest或者调用Web Service(连接报超时问题)

    .net 的 HttpWebRequest 或者 WebClient 在多线程情况下存在并发连接限制,这个限制在桌面操作系统如 windows xp , windows  7 下默认是2,在服务器操作 ...

  8. python多线程场景下print丢失

    python多线程情况下,print输出会出现丢失的情况,而logging模块的日志输出不会. 以下是示例代码,多运行几次就会发现这个有意思的现象 # coding:utf-8 import thre ...

  9. 多线程场景下如何使用 ArrayList

    ArrayList 不是线程安全的,这点很多人都知道,但是线程不安全的原因及表现,怎么在多线程情况下使用ArrayList,可能不是很清楚,这里总结一下. 1. 源码分析 查看 ArrayList 的 ...

随机推荐

  1. spring揭密学习笔记(2)-spring ioc容器:IOC的基本概念

    1. IoC的理念就是,让别人为你服务!2. 其实IoC就这么简单!原来是需要什么东西自己去拿,现在是需要什么东西就让别人送过来.一个生动的示例 3.三种依赖注入的方式 IoC模式最权威的总结和解释, ...

  2. windows服务器自动删除日志文件

    https://blog.csdn.net/u010050174/article/details/72510367 步骤: 1.新建 一个bat脚本 2.添加到window执行计划中,进行每日执行. ...

  3. hive 索引

    hive 有限的支持索引,不支持主键外键,可以对表添加索引,也可以为某个分区添加索引.维护索引也要额外的存储空间和计算资源. 创建索引需要指定索引处理器 如 as 'org.apache.hadoop ...

  4. Android ADB 基本命令

    ADB很强大,记住一些ADB命令有助于提高工作效率. 获取序列号: adb get-serialno 查看连接计算机的设备: adb devices 重启机器: adb reboot 重启到bootl ...

  5. leetcode212

    class Solution { public List<String> findWords(char[][] board, String[] words) { List<Strin ...

  6. Shell 编程 (变量和条件测试)

    变量: 1 . 变量声明 直接使用变量 + 赋值 #!/bin/bash NAME='HELLO WORD' echo $NAME 使用 declare 关键字声明 declare(选项)(参数) + ...

  7. ip黑白名单防火墙frdev的原理与实现

    汤之盘铭曰 苟日新 日日新 又日新 康诰曰 作新民 诗曰 周虽旧邦 其命维新 是故 君子无所不用其极 ——礼记·大学 在上一篇文章<DDoS攻防战 (二) :CC攻击工具实现与防御理论>中 ...

  8. Django笔记(2)Json字段处理

    1) Django里面让Model用于JSON字段,添加一个JSONField自动类型如下: [python] view plain copy class JSONField(models.TextF ...

  9. MongoDB 全部笔记

    1. MongoDB: 是NOSQL的一种, 特长是分布式用的,用于处理爬虫数据 2. mongoDB 与 redis mongoDB是最像关系型的非关系型数据,更加适用于大数据,redis则更倾向于 ...

  10. APP-8.2-Postman应用

    用户在开发或者调试网络程序或者是网页B/S模式的程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具.今天给大家介绍的这款网页调试工具不仅可以 ...