.Net多线程陷阱---Dictionary

昨天一到公司就发现负责的系统发了100多封报错邮件,而且随着上班同事的增加报错邮件一直在上升,心中那个纠结啊。大致看了一下报错邮件的内容,发现报错的问题集中在了某个方法,抛出的异常是IndexOutOfRangeException,以下是错误的摘要:

错误信息:
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)

由于System.Collections.Generic.Dictionary`2.Insert是Dictionary的私有方法,在Dictionary.Add方法里会使用到,而Dictionary又是.Net Framework很基本的类,因此System.Collections.Generic.Dictionary`2.Insert里本身有问题的可能性非常小。这样一来问题就定位到了****.GetEnumDescription方法里,一定是里面使用Dictionary的方法不对,一定是的。

用Reflector神器打开****.dll以后看到GetEnumDescritpion方法实现如下:

public static string GetEnumDescription(Enum value)
{
    if (enumCache.ContainsKey(value))
    {
        return enumCache[value];
    }
    DescriptionAttribute[] customAttributes = (DescriptionAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
    string str = (customAttributes.Length > 0) ? customAttributes[0].Description : value.ToString();
    enumCache.Add(value, str);
    return str;
}

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

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

毫无头绪啊。

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

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.

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方法里的内容看看:

private void Insert(TKey key, TValue value, bool add)
{
    int freeList;
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if (this.buckets == null)
    {
        this.Initialize(0);
    }
    int num = this.comparer.GetHashCode(key) & 0x7fffffff;
    int index = num % this.buckets.Length;
    for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
    {
        if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
        {
            if (add)
            {
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            this.entries[i].value = value;
            this.version++;
            return;
        }
    }
    if (this.freeCount > 0)
    {
        freeList = this.freeList;
        this.freeList = this.entries[freeList].next;
        this.freeCount--;
    }
    else
    {
        if (this.count == this.entries.Length)
        {
            this.Resize();
            index = num % this.buckets.Length;
        }
        freeList = this.count;
        this.count++;
    }
    this.entries[freeList].hashCode = num;
    this.entries[freeList].next = this.buckets[index];
    this.entries[freeList].key = key;
    this.entries[freeList].value = value;
    this.buckets[index] = freeList;
    this.version++;
}

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

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

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

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

int i = 0;
new Thread(() =>
{
    for (int k = 0; k < 10; k++)
    {
        i++;
    }
}).Start();

new Thread(() =>
{
    for (int k = 0; k < 10; k++)
    {
        i++;
    }
}).Start();
按正常情况来看,在这两个线程都执行完以后,i的值应该都是20,但是现实情况却是有一定概率i值会不等于20。
i++在执行的时候,CPU会得到类似这样的指令:
A: 表示这段指令是A线程上的,B: 表示这段指令是B线程上的

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

A: mov eax,[x]
A: inc eax
A: mov [x],eax
A: mov eax,[x]
A: inc eax
A: mov [x],eax
但是在两个线程中分别执行i++,情况就会变得非常复杂,CPU可能得到这样的指令:
A: mov eax,[x]
B: mov eax,[x]
A: inc eax
B: inc eax
A: mov [x],eax
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对象回收。

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





  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值