什么是实现线程安全字典的最佳方式?
我能够通过派生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
。
试图在内部进行同步几乎肯定是不够的,因为它的抽象级别太低。 假设您按如下方式单独进行线程安全的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); } }
那么当你从multithreading调用这个所谓的线程安全的代码时会发生什么呢? 它会一直工作吗?
if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); }
简单的答案是否定的。 在某些时候, Add
方法将会抛出一个exception,表示该键已经存在于字典中。 怎么可能用一个线程安全的字典,你可能会问? 那么只是因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在您调用ContainsKey
和Add
之间进行修改。
这意味着要正确地编写这种types的场景,你需要在字典外locking,例如
lock (mySafeDictionary) { if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } }
但是现在看到,当你不得不写外部锁代码的时候,你混淆了内部和外部的同步,这总是会导致一些问题,例如不清楚的代码和死锁。 所以最终你可能会更好:
-
使用正常的
Dictionary<TKey, TValue>
并在外部进行同步,在其上包含复合操作 -
编写一个新的线程安全封装与不同的接口(即不是
IDictionary<T>
),结合的操作,如AddIfNotContained
方法,所以你永远不需要结合它的操作。
(我倾向于和我一起去#1)
您不应该通过属性发布您的私人locking对象。 锁对象应该私下存在,仅用作集合点。
如果使用标准锁来certificate性能很差,那么Wintellect的Power Threading锁的集合可能非常有用。
您正在描述的实现方法有几个问题。
- 你不应该公开你的同步对象。 这样做可以让消费者抓住物品并locking物品,然后烤面包。
- 您正在实现一个线程安全类的非线程安全接口。 恕我直言,这将花费你的道路上
就我个人而言,我发现实现一个线程安全类的最好方法是通过不变性。 它确实减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和同步