添加词典条目时出现异常

时间:2011-11-16 11:49:37

标签: c# .net multithreading .net-4.0 dictionary

我们看到在IIS 7服务器上运行的ASP.NET上下文中的以下代码块中发生此异常。

1) Exception Information
*********************************************  
Exception Type: System.Exception  
Message: Exception Caught in Application_Error event
Error in: InitializationStatus.aspx  
Error Message:An item with the same key has already been added.  
Stack Trace:    at
System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)   
at CredentialsSession.GetXmlSerializer(Type serializerType)

这是发生异常的代码:

[Serializable()]
public class CredentialsSession
{
    private static Dictionary<string, System.Xml.Serialization.XmlSerializer> localSerializers = new Dictionary<string, XmlSerializer>();

    private System.Xml.Serialization.XmlSerializer GetXmlSerializer(Type serializerType)
    {
        string sessionObjectName = serializerType.ToString() + ".Serializer";

        if (Monitor.TryEnter(this))
        {
            try
            {
                if (!localSerializers.ContainsKey(sessionObjectName))
                {
                    localSerializers.Add(sessionObjectName, CreateSerializer(serializerType));
                }
            }
            finally
            {
                Monitor.Exit(this);
            }
        }
        return localSerializers[sessionObjectName];
    }

    private System.Xml.Serialization.XmlSerializer CreateSerializer(Type serializerType)
    {
        XmlAttributes xmlAttributes = GetXmlOverrides();

        XmlAttributeOverrides xmlOverrides = new XmlAttributeOverrides();
        xmlOverrides.Add(typeof(ElementBase), "Elements", xmlAttributes);

        System.Xml.Serialization.XmlSerializer serializer =
            new System.Xml.Serialization.XmlSerializer(serializerType, xmlOverrides);

        return serializer;
    }
}

Monitor.TryEnter 应该阻止多个线程同时进入块,并且代码检查字典以验证它不包含正在添加的密钥。

关于如何发生这种情况的任何想法?

3 个答案:

答案 0 :(得分:5)

尝试锁定localSerializers而不是this。顺便说一下,为什么你明确地使用Monitor?我看到的唯一原因是提供显然您没有使用的lock timeout,因此请简单地使用lock() statement来生成try / finally:

lock (localSerializers)
{
   if (!localSerializers.ContainsKey(sessionObjectName))                 
   {                     
      localSerializers.Add(
            sessionObjectName, 
            CreateSerializer(serializerType));                 
   } 
}

修改 由于您没有在标签中指定您使用.NET 4,我建议使用 ConcurrentDictionary<TKey, TValue>


Monitor.Enter() Method

  

使用C#try ... finally块(尝试...最后在Visual Basic中)来确保   您释放监视器,或使用C#锁定语句(SyncLock   Visual Basic中的语句,它包含了Enter和Exit方法   试试......最后阻止

答案 1 :(得分:5)

您的代码不是线程安全的。

  1. 您正在锁定this CredentialsSession实例,但访问可由多个CredentialsSession实例共享的静态字典。这解释了为什么您收到错误 - 两个不同的CredentialsSession实例正在尝试同时写入字典。

  2. 即使您按照@sll的答案中的建议将其更改为锁定静态字段,也不是线程安全的,因为在读取字典时您没有锁定。您需要ReaderWriterLockReaderWriterLockSlim才能有效地允许多个读者和一个作家。

    因此,您应该使用线程安全字典。 ConcurrentDictionary正如其他人所说,如果您使用的是.NET 4.0。如果不是,您应该实现自己的,或使用现有的实现,如http://devplanet.com/blogs/brianr/archive/2008/09/26/thread-safe-dictionary-in-net.aspx

  3. 您的评论建议您避免多次为同一类型调用CreateSerializer。我不知道为什么,因为性能优势可能是微不足道的,因为争用可能很少,并且在应用程序的生命周期内每种类型都不能超过一次。

    但如果你真的想要这个,你可以按照以下方式进行:

    var value;
    if (!dictionary.TryGetValue(key, out value))
    {
        lock(dictionary)
        {
            if(!dictionary.TryGetValue(key, out value))
            {
                value = CreateSerializer(...);
                dictionary[key] = value;
            }
        }
    }
    

    来自评论:

      

    如果我用ConcurrentDictionary实现它,并且每次只调用TryAdd(sessionObjectName,CreateSerializer(serializerType))。

    答案不是每次都调用TryAdd - 先检查它是否在字典中,然后添加,如果不是。更好的选择可能是使用带有Func参数的the GetOrAdd overload

答案 2 :(得分:1)

如果您使用的是.NET Framework 4或更高版本,我建议您改用ConcurrentDictionaryTryAdd方法可以保护您免受此类情况的影响,而无需使用锁定来丢弃代码:

localSerializers.TryAdd(sessionObjectName, CreateSerializer(serializerType))

如果您担心在不需要时调用CreateSerializer,则应改为使用AddOrUpdate

localSerializers.AddOrUpdate(
    sessionObjectName,
    key => CreateSerialzer(serializerType),
    (key, value) => value);

这将确保仅在需要生成新值时(当需要将其添加到字典中时)才调用该方法。如果它已经存在,则该条目将使用现有值“更新”。