锁定非线程安全对象,这是可以接受的做法吗?
我在前几天发布的评论中对此感到有些悲伤,所以我想发布这个问题,试图让人们告诉我我疯了,我会接受,或者告诉我我可能是对的,我也很乐意接受。我也可以接受介于两者之间的任何事情。
假设您有一个非线程安全的对象类型,例如 Dictionary
。为了便于讨论,我知道您也可以使用线程安全的 ConcurrentDictionary
考虑以下示例:
private static readonly Dictionary<int, string> SomeDictionary = new Dictionary<int, string>();
private static readonly object LockObj = new object();
public static string GetById(int id)
{
string result;
/** Lock Bypass **/
if (SomeDictionary.TryGetValue(id, out result)
{
return result;
}
lock (LockObj)
{
if (SomeDictionary.TryGetValue(id, out result)
{
return result;
}
SomeDictionary.Add(id, result = GetSomeString());
}
return result;
}
据我了解,这种锁定模式本质上同步了 Dictionary 的处理方式,这使得它是线程安全的。但是,我收到了一些负面评论,认为这实际上并没有使其线程安全。
所以,我的问题是,这种锁定模式对于多线程环境中的非线程安全对象是否可以接受?如果没有,使用什么更好的模式? (假设没有相同的线程安全 C# 类型)
I got some grief about this in a comment I posted the other day, so I wanted to post the question in an attempt for people to tell me that I'm crazy, which I'll accept, or tell me that I may be right, which I'll also gladly accept. I may also accept anything in between.
Let's say you have a non-thread-safe object type such as Dictionary<int, string>
. For the sake of argument, I know you can also use ConcurrentDictionary<int, string>
which is thread safe, but I want to talk about the general practice around non-thread-safe objects in a multi-threaded environment.
Consider the following example:
private static readonly Dictionary<int, string> SomeDictionary = new Dictionary<int, string>();
private static readonly object LockObj = new object();
public static string GetById(int id)
{
string result;
/** Lock Bypass **/
if (SomeDictionary.TryGetValue(id, out result)
{
return result;
}
lock (LockObj)
{
if (SomeDictionary.TryGetValue(id, out result)
{
return result;
}
SomeDictionary.Add(id, result = GetSomeString());
}
return result;
}
The locking pattern is called Double-Checked Locking, since the lock is actively bypassed if the dictionary is already initialized with that id. The "Add" method of the dictionary is called within the lock because we only want to call the method once, because it will throw an exception if you try to add an item with the same key.
It was my understanding that this locking pattern essentially synchronizes the way that Dictionary is handled, which allows it to be thread safe. But, I got some negative comments about how that doesn't actually make it thread safe.
So, my question is, is this locking pattern acceptable for non-thread-safe objects in a multi-threaded environment? If not, what would be a better pattern to use? (assuming there's not an identical C# type that is thread-safe)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
不,这不安全。
TryGetValue
方法根本就不是线程安全的,因此当对象在多个线程之间共享而无需锁定时,不应使用它。双重检查锁定模式仅涉及测试引用 - 虽然不能保证给出最新结果,但不会导致任何其他问题。与TryGetValue
相比,如果与Add
同时调用,它可以执行任何操作(例如引发异常、破坏内部数据结构)。就我个人而言,我只使用锁,但您可能可以使用
ReaderWriterLockSlim
。 (在大多数情况下,简单的锁定会更有效 - 但这取决于读取和写入操作需要多长时间以及争用情况。)No, this is not safe. The
TryGetValue
method simply isn't thread-safe, so you shouldn't use it when the object is shared between multiple threads without locking. The double-checked locking pattern involves just testing a reference - which while it isn't guaranteed to give an up to date result, won't cause any other problems. Compare that withTryGetValue
which could do anything (e.g. throw an exception, corrupt the internal data structure) if called at the same time as, say,Add
.Personally I'd just use a lock, but you could potentially use
ReaderWriterLockSlim
. (In most cases a simply lock will be more efficient - but it depends on how long the reading and writing operations take, and what the contentions are like.)这是不安全的,因为当字典处于不一致状态时,第二个线程可能会从
SomeDictionary
读取值。考虑以下场景:
Add
,但在该方法中途被中断。Add
的调用已经足够,该方法返回(或尝试返回)true
。现在可能会发生各种不好的事情。线程 B 可能会看到第一个
TryGetValue
(在锁之外)返回 true,但返回的值是无意义的,因为实际值尚未实际存储。另一种可能性是 Dictionary 实现意识到它处于不一致状态并抛出InvalidOperationException
。或者它可能不会抛出,它可能只是以损坏的内部状态继续。不管怎样,坏魔力。This isn't safe, because a second thread can potentially read the value from
SomeDictionary
while the dictionary is in an inconsistent state.Consider the following scenario:
Add
, but is interrupted partway through the method.Add
has gotten far enough that the method returns (or attempts to return)true
.Now a variety of bad things could happen. It's possible that Thread B sees the first
TryGetValue
(outside the lock) return true, but the value that's returned is nonsensical because the real value hasn't actually been stored yet. The other possibility is that the Dictionary implementation realizes that it's in an inconsistent state and throwsInvalidOperationException
. Or it might not throw, it might just continue with a corrupted internal state. Either way, bad mojo.只需删除第一个 TryGetValue 就可以了。
不要使用 ReaderWriterLock 或 ReaderWriterLockSlim,除非您执行的写入次数少于 20%,并且锁内的工作负载足够大,以至于并行读取很重要。作为示例,下面演示了当读/写操作很简单时,简单的 lock() 语句的性能优于使用读/写锁。
上述程序在我的电脑上输出以下结果:
Just remove the first TryGetValue and you'll be fine.
Do not use ReaderWriterLock or ReaderWriterLockSlim unless you are doing less than 20% writes AND the workload within the lock is significant enough that parallel reads will matter. As an example, the following demonstrates that a simple lock() statement will out-perform the use of either reader/writer locks when the read/write operation is simple.
The preceeding program outputs the following results on my PC: