实现线程安全字典的最佳方法是什么?
通过从IDictionary派生并定义私有SyncRoot对象,我能够在C中实现线程安全的Dictionary:
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue> { private readonly object syncRoot = new object(); private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>(); public object SyncRoot { get { return syncRoot; } } public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } } // more IDictionary members... }
然后,在整个使用者(多个线程)上锁定此SyncRoot对象:
例子:
lock (m_MySharedDictionary.SyncRoot) { m_MySharedDictionary.Add(...); }
我能够使其工作,但这导致了一些难看的代码。我的问题是,是否有更好,更优雅的方法来实现线程安全字典?
解决方案
我们不需要在使用者对象中锁定SyncRoot属性。我们在字典方法中拥有的锁就足够了。
详细说明:
最终发生的事情是,字典被锁定了比必要时间更长的时间。
在情况下,将发生以下情况:
说线程A在调用m_mySharedDictionary.Add之前获取了SyncRoot的锁定。然后,线程B尝试获取该锁,但被锁定。实际上,所有其他线程均被阻止。允许线程A调用Add方法。在Add方法中的lock语句中,允许线程A再次获取该锁,因为它已经拥有该锁。在方法内退出锁上下文,然后在方法外退出时,线程A释放了所有锁,从而允许其他线程继续执行。
我们可以简单地允许任何使用者调用Add方法,因为SharedDictionary类Add方法中的lock语句将具有相同的效果。此时,我们具有冗余锁定。如果必须对必须确保连续发生的字典对象执行两项操作,则只能在其中一种字典方法之外锁定SyncRoot。
正如Peter所说,我们可以将所有线程安全性封装在类中。我们需要谨慎对待公开或者添加的任何事件,确保它们在任何锁之外都被调用。
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue> { private readonly object syncRoot = new object(); private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>(); public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } OnItemAdded(EventArgs.Empty); } public event EventHandler ItemAdded; protected virtual void OnItemAdded(EventArgs e) { EventHandler handler = ItemAdded; if (handler != null) handler(this, e); } // more IDictionary members... }
编辑:MSDN文档指出,枚举本质上不是线程安全的。这可能是在类外公开同步对象的原因之一。解决该问题的另一种方法是提供一些对所有成员执行操作并锁定成员枚举的方法。问题是我们不知道传递给该函数的动作是否调用了字典的某个成员(这将导致死锁)。公开同步对象使使用者可以做出这些决定,并且不会在类中隐藏死锁。
我们不应该通过属性发布私有锁对象。锁定对象应仅作为一个集合点而私下存在。
如果使用标准锁证明性能不佳,则Wintellect的Power Threading锁集合将非常有用。
集合与同步
我们描述的实现方法存在几个问题。
- 我们不应该公开同步对象。这样做将使消费者容易抓住物品并对其进行锁定,然后我们就敬酒了。
- 我们正在使用线程安全类实现非线程安全接口。恕我直言,这将花费我们一路走来
就个人而言,我发现实现线程安全类的最佳方法是通过不变性。它确实减少了线程安全可能遇到的问题。查看Eric Lippert的博客以获取更多详细信息。
尝试内部进行同步几乎肯定是不够的,因为它的抽象级别太低。假设我们分别通过以下方式使Add
和ContainsKey
操作成为线程安全的:
public void Add(TKey key, TValue value) { lock (this.syncRoot) { this.innerDictionary.Add(key, value); } } public bool ContainsKey(TKey key) { lock (this.syncRoot) { return this.innerDictionary.ContainsKey(key); } }
那么,当我们从多个线程中调用这个所谓的线程安全的代码时,会发生什么呢?会一直正常吗?
if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); }
简单回答是不。在某些时候,Add
方法会抛出一个异常,表明该密钥已经存在于字典中。我们可能会问,如何使用线程安全的字典?好吧,因为每个操作都是线程安全的,所以两个操作的组合不是安全的,因为另一个线程可以在调用" ContainsKey"和" Add"之间对其进行修改。
这意味着要正确编写这种类型的场景,我们需要在字典外加一个锁,例如
lock (mySafeDictionary) { if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } }
但是现在,由于我们不得不编写外部锁定代码,因此我们正在混淆内部和外部同步,这总是会导致诸如代码不清晰和死锁之类的问题。因此,最终我们可能会更好:
- 使用普通的
Dictionary <TKey,TValue>
并在外部进行同步,将其上的复合操作括起来,或者 - 编写一个具有不同接口(即非IDictionary <T>)的新线程安全包装器,该包装器将诸如AddIfNotContained方法之类的操作组合在一起,因此我们无需从中组合操作。
(我自己倾向于和#1一起去)
支持并发的.NET 4.0类称为" ConcurrentDictionary"。