在调用DoStuffToDictionary(dictionaryTwo)时,可以安全地假设方法体内的操作包括索引器,LINQ扩展方法也是线程安全的吗?
用不同的方式表达这个问题,是否会出现交叉线程异常或死锁?
var dictionaryOne = new ConcurrentDictionary<int,int>();
var dictionaryTwo = new Dictionary<int,int>();
DoStuffToDictionary(dictionaryOne);
DoStuffToDictionary(dictionaryTwo);
void DoStuffToDictionary(IDictionary<int,int> items) {
// Manipulate dictionary
if (items[0] == -1) {
items[0] = 0; // Dumb example, but are indexers like this OK?
}
}
Run Code Online (Sandbox Code Playgroud)
这段代码有几个问题:
IDictionary接口可以通过任何类型的字典实现
您的示例当然不是线程安全的,因为您正在使用IDictionary<int,int>接口,这不保证任何线程安全.甚至你的代码都将a Dictionary和a ConcurrentDictionary传递给方法.
事务需要是原子的才能使它们成为线程安全
即使字典实现保证是线程安全的,你的代码也不会是因为你没有在两次调用之间锁定对字典的访问:
if (items[0] == -1) {
// <-- another thread can access items in this moment
items[0] = 0;
}
Run Code Online (Sandbox Code Playgroud)返回LINQ查询永远不是线程安全的
如果您使用LINQ返回IEnumerable或IQueriable从您的方法返回,那么锁几乎没有效果,除非您使用该ToList()方法立即计算表达式并缓存结果.这是因为LINQ仅"准备"查询以执行.如果IEnumerable从方法返回a,则在方法结束后(因此,在锁定之外)将访问实际字典.
这段代码最大的问题在于你传递的是IDictionary实例,这意味着代码的其他部分可以直接访问它,并且必须非常小心地锁定同一个锁对象实例.这是痛苦的,容易错误地正确实施,容易意外破坏,并且难以察觉(竞争条件可能在极少数情况下出现症状).
您可以做几件事来改进代码:
不要传递IDictionary,而是自己的接口(首选)
使字典成为实现一些自定义接口的类的私有成员,抽象所有操作,并使用锁来确保线程安全(或使用ConcurrentDictionary引擎盖) .这样,您可以确保使用相同的锁实例锁定所有调用.
不要使用界面,而是始终通过ConcurrentDictionary
这将是线程安全的,只要你使用特定的原子其中一个方法ConcurrentDictionary提供了(GetOrAdd,AddOrUpdate,等).使用像您在示例中所做的简单访问方法将不是线程安全的,这意味着您仍然需要小心它.另外的缺点是,如果您需要,您将无法抽象功能(无法包装/代理字典,您将无法使用其他字典实现).
传递IDictionary,并锁定字典本身(根本不推荐).
这是一个丑陋的黑客,不幸的是,它经常被使用.这意味着您需要在访问此词典的应用程序的每个部分执行此操作,并在此过程中进一步注意锁定多个操作.