C# 实现线程安全字典的最佳方法是什么?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/157933/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
What's the best way of implementing a thread-safe Dictionary?
提问by GP.
I was able to implement a thread-safe Dictionary in C# by deriving from IDictionary and defining a private SyncRoot object:
通过从 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...
}
I then lock on this SyncRoot object throughout my consumers (multiple threads):
然后我在整个消费者(多线程)中锁定这个 SyncRoot 对象:
Example:
例子:
lock (m_MySharedDictionary.SyncRoot)
{
m_MySharedDictionary.Add(...);
}
I was able to make it work, but this resulted in some ugly code. My question is, is there a better, more elegant way of implementing a thread-safe Dictionary?
我能够让它工作,但这导致了一些丑陋的代码。我的问题是,有没有更好、更优雅的方式来实现线程安全字典?
采纳答案by fryguybob
As Peter said, you can encapsulate all of the thread safety inside the class. You will need to be careful with any events you expose or add, making sure that they get invoked outside of any locks.
正如彼得所说,您可以将所有线程安全性封装在类中。你需要小心你公开或添加的任何事件,确保它们在任何锁之外被调用。
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...
}
Edit:The MSDN docs point out that enumerating is inherently not thread safe. That can be one reason for exposing a synchronization object outside your class. Another way to approach that would be to provide some methods for performing an action on all members and lock around the enumerating of the members. The problem with this is that you don't know if the action passed to that function calls some member of your dictionary (that would result in a deadlock). Exposing the synchronization object allows the consumer to make those decisions and doesn't hide the deadlock inside your class.
编辑:MSDN 文档指出枚举本质上不是线程安全的。这可能是在类外公开同步对象的原因之一。另一种方法是提供一些方法来对所有成员执行操作并锁定成员的枚举。这样做的问题是您不知道传递给该函数的操作是否调用了字典的某个成员(这会导致死锁)。公开同步对象允许使用者做出这些决定,并且不会隐藏类中的死锁。
回答by Peter Meyer
You don't need to lock the SyncRoot property in your consumer objects. The lock you have within the methods of the dictionary is sufficient.
您不需要锁定使用者对象中的 SyncRoot 属性。您在字典方法中拥有的锁就足够了。
To Elaborate:What ends up happening is that your dictionary is locked for a longer period of time than is necessary.
详细说明:最终会发生的情况是您的字典被锁定的时间超过了必要的时间。
What happens in your case is the following:
在您的情况下会发生以下情况:
Say thread A acquires the lock on SyncRoot beforethe call to m_mySharedDictionary.Add. Thread B then attempts to acquire the lock but is blocked. In fact, all other threads are blocked. Thread A is allowed to call into the Add method. At the lock statement within the Add method, thread A is allowed to obtain the lock again because it already owns it. Upon exiting the lock context within the method and then outside the method, thread A has released all locks allowing other threads to continue.
假设线程 A在调用 m_mySharedDictionary.Add之前获取了 SyncRoot 上的锁。然后线程 B 尝试获取锁但被阻塞。事实上,所有其他线程都被阻塞了。允许线程 A 调用 Add 方法。在 Add 方法中的 lock 语句中,线程 A 被允许再次获取锁,因为它已经拥有它。在方法内退出锁上下文,然后在方法外退出时,线程 A 已释放所有锁,允许其他线程继续。
You can simply allow any consumer to call into the Add method as the lock statement within your SharedDictionary class Add method will have the same effect. At this point in time, you have redundant locking. You would only lock on SyncRoot outside of one of the dictionary methods if you had to perform two operations on the dictionary object that needed to be guaranteed to occur consecutively.
您可以简单地允许任何使用者调用 Add 方法,因为 SharedDictionary 类 Add 方法中的锁定语句将具有相同的效果。此时,您有冗余锁定。如果您必须对需要保证连续发生的字典对象执行两个操作,则您只会在其中一个字典方法之外锁定 SyncRoot。
回答by Jonathan Webb
You shouldn't publish your private lock object through a property. The lock object should exist privately for the sole purpose of acting as a rendezvous point.
您不应该通过属性发布您的私有锁对象。锁定对象应该私下存在,其唯一目的是充当集合点。
If performance proves to be poor using the standard lock then Wintellect's Power Threadingcollection of locks can be very useful.
如果使用标准锁证明性能很差,那么 Wintellect 的Power Threading锁集合可能非常有用。
回答by MagicKat
回答by JaredPar
There are several problems with implementation method you are describing.
您描述的实现方法存在几个问题。
- You shouldn't ever expose your synchronization object. Doing so will open up yourself to a consumer grabbing the object and taking a lock on it and then you're toast.
- You're implementing a non-thread safe interface with a thread safe class. IMHO this will cost you down the road
- 您不应该公开您的同步对象。这样做会让消费者看到自己抓住物体并锁定它,然后你就干杯了。
- 您正在使用线程安全类实现非线程安全接口。恕我直言,这会让你付出代价
Personally, I've found the best way to implement a thread safe class is via immutability. It really reduces the number of problems you can run into with thread safety. Check out Eric Lippert's Blogfor more details.
就个人而言,我发现实现线程安全类的最佳方法是通过不变性。它确实减少了您在线程安全方面可能遇到的问题。查看Eric Lippert 的博客了解更多详情。
回答by Greg Beech
Attempting to synchronize internally will almost certainly be insufficient because it's at too low a level of abstraction. Say you make the Add
and ContainsKey
operations individually thread-safe as follows:
尝试在内部同步几乎肯定是不够的,因为它的抽象级别太低。假设您使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);
}
}
Then what happens when you call this supposedly thread-safe bit of code from multiple threads? Will it always work OK?
那么当你从多个线程调用这个所谓的线程安全的代码时会发生什么?它会一直正常工作吗?
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
The simple answer is no. At some point the Add
method will throw an exception indicating that the key already exists in the dictionary. How can this be with a thread-safe dictionary, you might ask? Well just because each operation is thread-safe, the combination of two operations is not, as another thread could modify it between your call to ContainsKey
and Add
.
简单回答是不。在某些时候,该Add
方法将抛出一个异常,表明该键已存在于字典中。您可能会问,这怎么可能与线程安全字典有关?好吧,仅仅因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在您调用ContainsKey
和之间修改它Add
。
Which means to write this type of scenario correctly you need a lock outsidethe dictionary, e.g.
这意味着写这种类型的场景正确,您需要一个锁外面的字典,如
lock (mySafeDictionary)
{
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
}
But now, seeing as you're having to write externally locking code, you're mixing up internal and external synchronisation, which always leads to problems such as unclear code and deadlocks. So ultimately you're probably better to either:
但是现在,当您不得不编写外部锁定代码时,您正在混淆内部和外部同步,这总是会导致诸如代码不清楚和死锁之类的问题。所以最终你可能更好地选择:
Use a normal
Dictionary<TKey, TValue>
and synchronize externally, enclosing the compound operations on it, orWrite a new thread-safe wrapper with a different interface (i.e. not
IDictionary<T>
) that combines the operations such as anAddIfNotContained
method so you never need to combine operations from it.
在
Dictionary<TKey, TValue>
外部使用普通和同步,将复合操作封闭在其上,或使用不同的接口(即 not
IDictionary<T>
)编写一个新的线程安全包装器,它组合了诸如AddIfNotContained
方法之类的操作,因此您永远不需要组合来自它的操作。
(I tend to go with #1 myself)
(我自己倾向于#1)
回答by Hector Correa
The .NET 4.0 class that supports concurrency is named ConcurrentDictionary
.
支持并发的 .NET 4.0 类名为ConcurrentDictionary
.
回答by verbedr
Just a thought why not recreate the dictionary? If reading is a multitude of writing then locking will synchronize all requests.
只是一个想法为什么不重新创建字典?如果读取是大量写入,则锁定将同步所有请求。
example
例子
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);
}