.NET Dictionary:潜在的并发问题?

时间:2009-01-29 09:34:27

标签: c# .net concurrency dictionary locking

我正在努力维护.NET项目,我遇到了一些麻烦,我很乐意与你们分享=)

问题代码:

if( evilDict.Count < 1 )
{
    foreach (Item item in GetAnotherDict())
        if (!evilDict.containsKey(item.name.ToLower().Trim()))
            evilDict.add(item.name.ToLower().Trim(), item.ID);
}

尽管contains() - 检查,我收到一个ArgumentException告诉我已经添加了一个具有相同键的项目。我们只是在生产中遇到这个问题,从未进行过测试,这让我怀疑是并发问题。我想知道的是:

  • 您认为这是并发问题吗?
  • 我该如何解决?
  • 我的修复是否可行(见下文)?
  • 这是我对.NET的第一次尝试,字典通常是问题的根源吗?

这是我的潜在修复,取代了dictionary.add()的东西

protected static void DictAddHelper(Dictionary<String, int> dict, String key, int value)
{
    lock (dict)
    {
        key = key.ToLower().Trim();
        if (dict.ContainsKey(key) == false)
        {
            try
            {
                dict.Add(key, value);
            }
            catch (ArgumentException aex)
            {
                StringBuilder debugInfo = new StringBuilder();
                debugInfo.AppendLine("An argumentException has occured: " + aex.Message);
                debugInfo.AppendLine("key = " + key);
                debugInfo.AppendLine("value = " + value);
                debugInfo.AppendLine("---Dictionary contains---");

                foreach (String k in dict.Keys)
                    debugInfo.AppendLine(k + " = " + dict[k]);

                log.Error(debugInfo, aex);
            }
        }
    }
}

修改

建议不要求我进行Dict类的线程安全实现更好,因为这将是一个非常大的重构,不会是一个非常受欢迎的建议=)

EDIT2:

我试过

lock (((IDictionary)dict).SyncRoot)

但是我得到了

Error   28  Using the generic type 'System.Collections.Generic.IDictionary<TKey,TValue>' requires '2' type arguments    

然后我试试这个:

lock (((IDictionary<String, int>)dict).SyncRoot)

错误:

Error   28  'System.Collections.Generic.IDictionary<string,int>' does not contain a definition for 'SyncRoot'

最终编辑(我猜):

感谢所有答案!

现在,我想知道的就是这个。我的方法(DictAddHelper)会起作用,如果没有,为什么?

11 个答案:

答案 0 :(得分:6)

如果您怀疑访问字典时遇到并发问题,那么您的修补程序将没有任何用处。它将解决您遇到的特定问题,但是如果您同时访问该字典,将来会遇到更多问题。

在修改字典时,请考虑锁定对字典的访问。

答案 1 :(得分:3)

使用ng5000的例子,修改为建议的锁定策略会给我们:

public static class ThreadSafeDictionary
{
    private static readonly Dictionary<string, int> dict = new Dictionary<string, int>();

    public static void AddItem(string key, int value)
    {
        lock (((IDictionary)dict).SyncRoot)
        {
            if (!dict.ContainsKey(key))
                dict.Add(key, value);
        }
    }
}
享受。 。 。

吉米亨德里

编辑:请注意,为了理解这个例子,类必须是静态的! ;)

答案 2 :(得分:2)

第一个代码(假设Dictionary类型是System.Collections.Generic.Dictionary)将无法编译。没有public contains(也不包含Contains方法)。

那就是说,你可能会遇到并发问题。正如你暗示的另一个线程可以在ContainsKey检查之后和插入之前修改字典。要纠正这个问题,可以采用锁定语句。

有一点 - 我更希望看到包含在线程安全类中的字典,例如(警告:不完整,不打算作为参考代码,可以改进):

public class ThreadSafeDictionary
{
    private Dictionary<string, int> dict = new Dictionary<string, int>();
    private object padlock = new object();

    public void AddItem( string key, int value )
    {
        lock ( padlock )
        {
            if( !dict.ContainsKey( key ) )
                dict.Add( key, value );
        }
    }
}

如何实现线程安全词典已完整涵盖here

答案 3 :(得分:2)

正如Megacan所说,您可能希望专注于解决您的解决方案中可能存在的任何并发问题。

我建议使用SynchronizedKeyedCollection,尽管这可能对你来说很重要,因为calss的成员与字典的成员不同。

答案 4 :(得分:1)

不应该

if (!evilDict.contains(item.name.ToLower().Trim()))

if (!evilDict.ContainsKey(item.name.ToLower().Trim()))

答案 5 :(得分:1)

很难说它是否确实是一个并发问题。 如果多个线程正在访问字典,那么它确实可能是一个并发问题,但我认为你应该添加一些跟踪代码来找出谁是罪魁祸首。

如果多个线程正在访问字典,那么您应该确保在一个原子事务中调用Contains(或ContainsKey)和Add方法。 为了做到这一点,你应该确实在锁中调用这两个方法。

lock( dict.SyncRoot )
{
   if( dict.Contains( .. ) == false )
      dict.Add ( );
}

答案 6 :(得分:1)

    lock (((IDictionary)dict).SyncRoot)
    {
        if(!dict.Contains( .. ))
        dict.Add ( );
    }

这对我有用(请参阅下面的更新ng5000示例):)

答案 7 :(得分:1)

.NET Framework中内置了一个线程安全的字典类,它已经为解决您的问题提供了良好的开端,这可能确实与并发相关。

这是一个名为SynchronizedKeyedCollection(K, T)的抽象类,您可以从中派生并添加一个包含对Contains的调用的方法,然后在锁定base.SyncRoot的锁内添加。

答案 8 :(得分:1)

Ace - 奇怪的是它对你的代码不起作用,它肯定按照例子(我知道这对你没有帮助,只是一个好奇心!)。

希望你能解决这个问题,我相信这是失败的一个相当简单的原因。你甚至可能想尝试代替你的代码来看看它是否是其他问题。

答案 9 :(得分:1)

度Acc,

创建一个新的WindowsFormsApplication(.Net 2.0)并将代码粘贴到Form1.cs代码中。 (您可能必须将命名空间的名称从WindowsFormsApplication1更改为系统默认的名称)。此外,向窗体添加一个命令按钮。无论如何,这一切都是:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private readonly Dictionary<string, int> localDict1 = new Dictionary<string, int>();
        private readonly Dictionary<string, string> localDict2 = new Dictionary<string, string>();

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // use two different dictionaries just to show it working with
            // different data types (i.e. we could use class objects too)
            if (localDict1 != null)
            {
                ThreadSafeDictionaryStatic.AddItem(localDict1, "New Item :1", 1);
                ThreadSafeDictionaryStatic.AddItem(localDict1, "New Item :2", 2);
            }
            if (localDict2 != null)
                ThreadSafeDictionaryStatic.AddItem(localDict2, "New Item :1", "this is not a number");
        }
    }

    public static class ThreadSafeDictionaryStatic
    {

        public static void AddItem<T>(IDictionary<string, T> dict, string key, T value)
        {
            if (dict == null) return;
            lock (((IDictionary)dict).SyncRoot)
            {
                if (!dict.ContainsKey(key))
                    dict.Add(key, value);
                else
                {
                    // awful and we'd never ever show a message box in real life - however!!
                    var result = dict[key];
                    MessageBox.Show(String.Format("Key '{0}' is already present with a value of '{1}'", key, result));
                }
            }
        }

        public static T GetItem<T>(IDictionary<string, T> dict, string key)
        {
            if (dict == null) return default(T);
            if (dict.ContainsKey(key))
                return dict[key];
            else
                return default(T);
        }

        public static bool Remove<T>(IDictionary<string, T> dict, string key)
        {
            if (dict == null) return false;
            lock (((IDictionary)dict).SyncRoot)
            {
                if (dict.ContainsKey(key))
                    return dict.Remove(key);
            }
            return false;
        }
    }
}

让我知道这是怎么回事......

编辑:重新上课以匹配您的方法签名,也使用泛型,因为您希望该方法接受任何类型的对象。您可以通过删除<T>引用等来轻松更改它。

答案 10 :(得分:0)

重构不一定非常痛苦或难以执行。只需进行以下重构:

1)为您的字典创建一个包装器容器类,它实现与字典

相同的接口

2)寻找你的字典的声明并应用一个重构改变声明的类型(到你刚创建的那个)

3)尝试构建,此时,如果你的包装器实现了所有的接口成员,你不应该得到任何编译错误

4)在您的词典的访问者上,将所有内容包装在锁中或您想要应用的任何同步策略中