我能够通过从IDictionary派生并定义一个私有的SyncRoot对象,在C#中实现一个线程安全的Dictionary:
public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary (); 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(...); }
我能够使它工作,但这导致了一些丑陋的代码.我的问题是,是否有更好,更优雅的方式来实现线程安全的字典?
命名支持并发的.NET 4.0类ConcurrentDictionary
.
试图在内部进行同步几乎肯定是不够的,因为它的抽象级别太低了.假设您按照以下方式单独使用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
并在外部同步,将复合操作包含在其上,或
编写一个具有不同接口(即不是IDictionary
)的新线程安全包装器,它结合了诸如AddIfNotContained
方法之类的操作,因此您永远不需要组合它的操作.
(我倾向于自己选择#1)
正如Peter所说,您可以封装类中的所有线程安全性.您需要小心处理您公开或添加的任何事件,确保它们在任何锁定之外被调用.
public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary (); 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的博客以获取更多详细信息。