什么是最佳的方式执行一个线程安全的字典?
-
03-07-2019 - |
题
我能够实现一个线程安全的字典在C#通过源自IDictionary和定义的私人SyncRoot对象:
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(...);
}
我能够使它的工作,但是这造成了一些丑陋的代码。我的问题是,是否有一个更好的,更优雅的方式,实施一个线程安全的字典?
解决方案
彼得说,你可以封装所有的线的安全内部的类。你会需要注意的任何事件,获得或增加,确保他们获得援引以外的任何锁。
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文件指出,列举了本来就是无线的安全。这可以是一个原因,暴露出一个同步的对象之外。另一种做法将提供一些方法,用于执行行动的所有成员和锁绕枚举的成员。这个问题是,你不知道如果动作传递给这一职能的呼吁一些部件的您的词典(这将导致僵局).暴露的同步目的可以让消费者作出这些决定并没有隐藏的僵局内部。
其他提示
支持并发的.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);
}
简单的答案是否定的。在某些时候,Dictionary<TKey, TValue>
方法将抛出一个异常,表明该密钥已经存在于字典中。您可能会问,如何使用线程安全的字典?好吧,因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在您对IDictionary<T>
和AddIfNotContained
的调用之间修改它。
这意味着要正确地编写这种类型的场景,你需要锁定在字典之外,例如
lock (mySafeDictionary)
{
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
}
但现在,当你不得不编写外部锁定代码时,你会混淆内部和外部同步,这总是会导致诸如代码不清和死锁等问题。所以最终你可能会更好:
-
使用普通<=>并在外部进行同步,将复合操作封闭在其上,或
-
使用不同的接口(即不是<=>)编写一个新的线程安全包装器,它结合了诸如<=>方法之类的操作,因此您永远不需要组合它的操作。
醇>
(我倾向于自己选择#1)
您不应通过属性发布私有锁对象。锁定对象应该私有存在,仅用作集合点。
如果使用标准锁定表现不佳,那么Wintellect的 Power Threading 锁定系列可以非常有用。
您正在描述的实现方法存在一些问题。
- 您不应该公开同步对象。这样做会打开一个消费者抓住物体并锁上它然后你敬酒。
- 您正在使用线程安全类实现非线程安全接口。恕我直言,这将花费你的路上 醇>
就个人而言,我发现实现线程安全类的最佳方法是通过不变性。它确实减少了线程安全可能遇到的问题。查看 Eric Lippert的博客了解更多详情。
您无需在使用者对象中锁定SyncRoot属性。你在字典方法中的锁就足够了。
详细说明: 最终发生的事情是你的字典被锁定的时间比必要的时间长。
您的情况如下:
假设线程A在调用m_mySharedDictionary.Add之前获取SyncRoot 上的锁定。线程B然后尝试获取锁定但被阻止。实际上,所有其他线程都被阻止了。允许线程A调用Add方法。在Add方法中的lock语句中,允许线程A再次获取锁,因为它已经拥有它。在退出方法中的锁定上下文然后在方法外部时,线程A释放了所有锁,允许其他线程继续。
您可以简单地允许任何使用者调用Add方法,因为SharedDictionary类中的lock语句Add方法将具有相同的效果。此时,您有冗余锁定。如果必须对需要保证连续发生的字典对象执行两个操作,则只能在其中一个字典方法之外锁定SyncRoot。
为什么不重新创建字典?如果读取是大量写入,则锁定将同步所有请求。
例如
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);
}