C# System.Collections.Generic.Dictionary`2.Insert - 已经添加了相同键的项

4
这个问题涉及到一个 .Net Framework 4.5 MVC Web 应用程序。
我有一段我们继承并使用多年的代码块,它通用地将 DataTable 转换为 List<T>,其中一个私有方法获取一个泛型类的属性列表,例如: 原始代码
    private static Dictionary<Type, IList<PropertyInfo>> typeDictionary = new Dictionary<Type, IList<PropertyInfo>>();

    public static IList<PropertyInfo> GetPropertiesForType<T>()
    {
        //variables
        var type = typeof(T);

        //get types
        if (!typeDictionary.ContainsKey(typeof(T)))
        {
            typeDictionary.Add(type, type.GetProperties().ToList());
        }

        //return
        return typeDictionary[type];
    }


这段代码并没有什么特别激动人心的内容,它只是确保typeDictionary中不包含该键(类型),然后将其添加到字典中(key=type,value=properties),以便我们稍后可以访问它们。

我们通常将此代码用于任何类型的“模型”对象,但对于这个特定的示例,这是我在两个不同场合遇到过麻烦的地方。

模型对象

public class GetApprovalsByUserId
{
    // constructor
    public GetApprovalsByUserId()
    {
        TicketId = 0;
        ModuleName = string.Empty;
        ModuleIcon = string.Empty;
        ApprovalType = string.Empty;
        VIN = string.Empty;
        StockNumber = string.Empty;
        Year = 0;
        Make = string.Empty;
        Model = string.Empty;
        Services = string.Empty;
        RequestedDate = DateTime.MinValue;
    }

    // public properties
    public int TicketId { get; set; }
    public string ModuleName { get; set; }
    public string ModuleIcon { get; set; }
    public string ApprovalType { get; set; }
    public string VIN { get; set; }
    public string StockNumber { get; set; }
    public int Year { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public string Services { get; set; }
    public DateTime RequestedDate { get; set; }
}


再次强调,在这个特定的模型类中没有什么真正重要的事情发生,也没有任何与我们在其他类中使用的不同之处。

就像我说的,我们在几个项目中通用地使用这段代码,从来没有出现过问题,但是在过去的一天里,我们已经两次遇到了以下异常:

已经添加了具有相同键的项。

at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at Utilities.Extensions.GetPropertiesForType[T]()
at Utilities.Extensions.ToObject[T](DataRow row)
at Utilities.Extensions.ToList[T](DataTable table)


如果有帮助的话,您可以在此处查看包含扩展方法的完整Extensions.cs类(静态):

https://pavey.azurewebsites.net/resources/Extensions.txt

我的问题是:

  1. 考虑到代码已经进行了 !typeDictionary.ContainsKey(typeof(T)) 检查,它怎么可能通过这个测试,然后在 typeDictionary.Add(type, type.GetProperties().ToList()); 调用时失败?

  2. 为什么会如此不稳定?它似乎99%的时间都能正常工作,使用相同的代码、相同的类(如上所示的 GetApprovalsByUserId),在任何其他项目或任何其他模型类中都从未失败过。

我们无法使用完全相同的代码、模型、数据或任何其他环境再现此问题,因此不确定如何比已经存在的安全保护措施更进一步地保护这段代码。

我想到的一个想法是将代码更改为:

建议的代码更改

    private static Dictionary<Type, IList<PropertyInfo>> typeDictionary = new Dictionary<Type, IList<PropertyInfo>>();

    public static IList<PropertyInfo> GetPropertiesForType<T>()
    {
        //variables
        var type = typeof(T);
        IList<PropertyInfo> properties = null;

        //get types
        try
        {
            if (!typeDictionary.ContainsKey(type))
            {
                typeDictionary.Add(type, type.GetProperties().ToList());
            }
        }
        catch
        {
        }

        // try get value
        typeDictionary.TryGetValue(type, out properties);

        // return
        return properties;
    }


但由于我无法重现这个错误,所以我并不确定这是否完全可靠。我的想法是,这可能只是ContainsKey的一些奇怪问题,特别是使用typeof(T)作为“键”,这使得它可以在奇怪的情况下通过测试,而它实际上不应该通过测试,但由于已知该键已经存在,所以添加操作失败了。因此,如果我尝试/捕获它,如果ContainsKey错误地告诉我它不存在,当实际上它存在时,添加操作仍将失败,但我会捕获它,并继续执行,然后我可以尝试解析以获取值,一切应该都很好。

感谢您提供任何想法、建议,或具体说明如何重现上面所示的原始代码的问题,以及推荐改进方法以保护它。


5
考虑到代码已经执行了一个 !typeDictionary.ContainsKey(typeof(T)) 的检查,那么它怎么可能通过这个测试,但在 typeDictionary.Add(type, type.GetProperties().ToList()) 调用上失败呢? -- 竞态条件? - Robert Harvey
3
可能是竞态条件导致的吗?这个代码似乎在99%的情况下都能正常工作,使用相同的代码和类(如上所示的GetApprovalsByUserId),并且在任何其他项目或模型类中也从未失败过。 - Robert Harvey
一定是竞态条件。您需要在检查键是否已存在并插入它的代码周围加上“锁定”。 - Matt Burland
哦,模拟竞态条件非常容易,只是不要在你的应用程序中这样做。下面的大部分代码足够正统,可以放心使用。但如果您确实需要证明,请将其放入小型控制台应用程序中并使用一些线程来进行测试。 - Robert Harvey
@Matt:是的,这些测试可能很麻烦。正如Robert建议的那样,你最好组建一个测试平台,只需将一堆线程投入到那段代码中即可。我自己曾经遇到过一个非常类似的问题。一段时间以前,我在Web API调用中遇到了一个空引用问题。这不是一个关键的现有问题(因为我实际上没有使用Add),而是一个字典调整大小的问题。 - Matt Burland
显示剩余2条评论
2个回答

4
你面临的问题是并发访问。在检查和插入字典之间,另一个线程已经添加了该类型,导致第二次插入失败。
为了解决这个问题,你有两个选择:要么使用锁(如其他答案中所提到的),要么使用ConcurrentCollection。
using System.Collections.Concurrent;
private static ConcurrentDictionary<Type, IList<PropertyInfo>> typeDictionary = new ConcurrentDictionary<Type, IList<PropertyInfo>>();

public static IList<PropertyInfo> GetPropertiesForType<T>()
{
    //variables
    var type = typeof(T);

    typeDictionary.TryAdd(type, type.GetProperties().ToList());

    //return
    return typeDictionary[type];
}

如果该值尚不存在,则此操作将添加该值并返回true,否则不执行任何操作并返回false。

3
这是一篇关于ConcurrentDictionary比锁定字典更优势的好文章。 - David L

1
你需要一个对象来进行锁定:
private object lockObj = new object();

然后在添加密钥之前需要锁定:

if (!typeDictionary.ContainsKey(typeof(T)))
{
    lock(lockObj) 
    {
        if (!typeDictionary.ContainsKey(typeof(T)))
        {
            typeDictionary.Add(type, type.GetProperties().ToList());
        }
    }
}

如果正在添加同一个键,则会使任何正在寻找相同键的其他线程等待。在锁内再次检查ContainsKey,因为当停止的线程最终获取锁时,另一个线程可能已经插入了该键。

这是双重检查锁定的示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接