什么是实现线程安全字典的最佳方式?

我能够通过派生IDictionary和定义一个私有的SyncRoot对象在C#中实现一个线程安全的字典:

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... } 

然后,我在我的消费者(multithreading)上locking这个SyncRoot对象:

例:

 lock (m_MySharedDictionary.SyncRoot) { m_MySharedDictionary.Add(...); } 

我能够使它工作,但是这导致了一些丑陋的代码。 我的问题是,是否有一个更好,更优雅的方式来实现一个线程安全的字典?

正如彼得所说的,你可以把所有的线程安全封装在课堂上。 你需要小心你公开或添加的任何事件,确保它们在任何锁之外被调用。

 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文档指出,枚举本质上不是线程安全的。 这可能是在您的类外部公开同步对象的一个​​原因。 解决这个问题的另一种方法是提供一些方法来执行所有成员的操作,并locking成员的枚举。 这个问题是,你不知道传递给该函数的动作是否调用了你的字典的一些成员(这会导致死锁)。 公开同步对象允许消费者做出这些决定,并且不会隐藏类中的死锁。

支持并发的.NET 4.0类名为ConcurrentDictionary

试图在内部进行同步几乎肯定是不够的,因为它的抽象级别太低。 假设您按如下方式单独进行线程安全的AddContainsKey操作:

 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); } } 

那么当你从multithreading调用这个所谓的线程安全的代码时会发生什么呢? 它会一直工作吗?

 if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } 

简单的答案是否定的。 在某些时候, Add方法将会抛出一个exception,表示该键已经存在于字典中。 怎么可能用一个线程安全的字典,你可能会问? 那么只是因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在您调用ContainsKeyAdd之间进行修改。

这意味着要正确地编写这种types的场景,你需要在字典locking,例如

 lock (mySafeDictionary) { if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } } 

但是现在看到,当你不得不写外部锁代码的时候,你混淆了内部和外部的同步,这总是会导致一些问题,例如不清楚的代码和死锁。 所以最终你可能会更好:

  1. 使用正常的Dictionary<TKey, TValue>并在外部进行同步,在其上包含复合操作

  2. 编写一个新的线程安全封装与不同的接口(即不是IDictionary<T> ),结合的操作,如AddIfNotContained方法,所以你永远不需要结合它的操作。

(我倾向于和我一起去#1)

您不应该通过属性发布您的私人locking对象。 锁对象应该私下存在,仅用作集合点。

如果使用标准锁来certificate性能很差,那么Wintellect的Power Threading锁的集合可能非常有用。

您正在描述的实现方法有几个问题。

  1. 你不应该公开你的同步对象。 这样做可以让消费者抓住物品并locking物品,然后烤面包。
  2. 您正在实现一个线程安全类的非线程安全接口。 恕我直言,这将花费你的道路上

就我个人而言,我发现实现一个线程安全类的最好方法是通过不变性。 它确实减less了可以在线程安全中遇到的问题的数量。 查看Eric Lippert的博客了解更多详情。

您不需要locking消费者对象中的SyncRoot属性。 您在字典的方法中拥有的锁已经足够了。

详细说明:最终发生的情况是,您的字典被locking的时间超过了必要的时间。

你的情况发生了以下几点:

说线程A在调用m_mySharedDictionary.Add 之前获取SyncRoot上的锁。 线程B然后尝试获取锁但被阻塞。 实际上,所有其他线程都被阻塞了。 线程A被允许调用Add方法。 在Add方法的lock语句中,线程A被允许再次获得锁,因为它已经拥有它。 在退出方法内的locking上下文并且然后在方法外,线程A已经释放所有的锁,允许其他线程继续。

您可以简单地允许任何使用者调用Add方法,因为SharedDictionary类的Add方法中的locking语句具有相同的效果。 在这个时候,你有多余的locking。 如果必须对需要保证连续出现的字典对象执行两个操作,则只能locking其中一个字典方法之外的SyncRoot。

只是一个想法,为什么不重新创build字典? 如果阅读是大量写作,那么locking将同步所有请求。

  private static readonly object Lock = new object(); private static Dictionary<string, string> _dict = new Dictionary<string, string>(); private string Fetch(string key) { lock (Lock) { string returnValue; if (_dict.TryGetValue(key, out returnValue)) return returnValue; returnValue = "find the new value"; _dict = new Dictionary<string, string>(_dict) { { key, returnValue } }; return returnValue; } } public string GetValue(key) { string returnValue; return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key); } 

collections和同步