【问题标题】:Exception when adding Dictionary entry添加字典条目时出现异常
【发布时间】:2011-12-30 08:56:35
【问题描述】:

我们在 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 应该防止多个线程同时进入块,并且代码正在检查 Dictionary 以验证它不包含正在添加的键。

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

【问题讨论】:

  • +1,用于解释如何在字典中查找重复项的问题。

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


【解决方案1】:

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

  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

您的 cmets 建议您避免多次为同一类型调用 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

【讨论】:

  • 我没有注意到字典是静态的!这解释了很多,谢谢!我正在使用 ConcurrentDictionary 重新实现它。
  • 假设 GetXmlSerializer 被频繁调用以从 Dictionary 中检索值,如果我使用 ConcurrentDictionary 实现它并且每次都简单地调用 TryAdd(sessionObjectName, CreateSerializer(serializerType)) ,那么每次 GetXmlSerializer 时都会调用 CreateSerializer叫。看来我想避免这种情况。一旦为每个键值填充了字典,就没有理由再调用 CreateSerializer。对吗?
  • +1 因为这个答案完全描述了现有问题并建议如何针对不同情况解决它
  • 我接受这个答案,因为它描述了现有问题的性质。其他答案对解决此问题也非常有帮助。我最终用 ConcurrentDictionary 实现了这一点,并按照 Fredrik Mörk 的建议使用了它的 AddOrUpdate 方法。
  • 更正:我实际上是用 GetOrAdd 方法实现的。
【解决方案2】:

尝试锁定localSerializers 而不是this。顺便说一句,您为什么要明确使用 Monitor ?我看到的唯一原因是提供 lock timeout 显然你没有使用,所以只需使用 lock() statement 代替这也会生成 try/finally:

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

编辑: 由于您没有在使用 .NET 4 的标签中指定我建议使用 ConcurrentDictionary&lt;TKey, TValue&gt;


Monitor.Enter() Method:

使用 C# try...finally 块(在 Visual Basic 中为 Try...Finally)确保 释放监视器,或使用 C# 锁定语句(SyncLock Visual Basic 中的语句),它将 Enter 和 Exit 方法包装在 试一试……终于阻止了

【讨论】:

  • 感谢您的回复!最初不是我的代码,所以我无法回答为什么它显式使用 Monitor。锁定受保护的资源而不是这个是非常有意义的。我会试试这个,谢谢!
  • @Avalanchis :请参阅更新答案的编辑部分,我还为您的问题添加了 .NET 4 标签,这非常重要
  • 我喜欢使用 ConcurrentDictionary 的想法,但我担心对 CreateSerializer 的不必要调用。如果我想避免每次都调用 CreateSerializer,我似乎仍然需要调用 ContainsKey 来查看密钥是否存在,然后再调用 TryAdd。对吗?
  • 也许我可以使用 GetOrAdd 并提供 valueFactory 参数来生成值?
  • 是的,你是对的,当你需要返回序列化程序时,最好检查它是否存在,然后从字典中请求它并返回。因此,只需使用 Containskey(),因为在执行 GetOrAdd() 之前,无论如何都会调用 CreateSerializer() 检查密钥是否存在
【解决方案3】:

如果您使用 .NET Framework 4 或更高版本,我建议您改用 ConcurrentDictionaryTryAdd 方法可以让您远离这种情况,而无需在代码中乱扔锁:

localSerializers.TryAdd(sessionObjectName, CreateSerializer(serializerType))

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

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

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

【讨论】:

  • 我们使用的是 .NET 4。检查 ConcurrentDictionary 是否包含要首先添加的键是否仍然有意义?如果密钥已经存在,我担心 TryAdd 可能会不必要地调用 CreateSerializer。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-01
  • 2015-08-29
  • 1970-01-01
  • 1970-01-01
  • 2020-05-04
  • 2014-05-01
  • 1970-01-01
相关资源
最近更新 更多