Recently I've seen some C# projects that use a double-checked-lock pattern on a Dictionary
. Something like this:
private static readonly object _lock = new object();
private static volatile IDictionary<string, object> _cache =
new Dictionary<string, object>();
public static object Create(string key)
{
object val;
if (!_cache.TryGetValue(key, out val))
{
lock (_lock)
{
if (!_cache.TryGetValue(key, out val))
{
val = new object(); // factory construction based on key here.
_cache.Add(key, val);
}
}
}
return val;
}
This code is incorrect, since the Dictionary
can be "growing" the collection in _cache.Add()
while _cache.TryGetValue
(outside the lock) is iterating over the collection. It might be extremely unlikely in many situations, but is still wrong.
Is there a simple program to demonstrate that this code fails?
Does it make sense to incorporate this into a unit test? And if so, how?
In this example, exception #1 is thrown almost instantly on my machine:
However, the exact behaviour of code that is not designed to be thread-safe is unpredictable.
You cannot rely on it. So the double-checking code is indeed broken.
I'm not sure if I'd unit test this, though, as testing concurrent code (and getting it right) is much more complicated than writing the concurrent code in the first place.
Clearly the code is not threadsafe. What we have here is a clear case of the hazards of premature optimization.
Remember, the purpose of the double-checked locking pattern is to improve the performance of code by eliminating the cost of the lock. If the lock is uncontested it is incredibly cheap already. Therefore, the double-checked locking pattern is justified only in the cases (1) where the lock is going to be heavily contested, or (2) where the code is so incredibly performance-sensitive that the cost of an unconstested lock is still too high.
Clearly we are not in the second case. You're using a dictionary for heaven's sake. Even without the lock it will be doing lookups and comparisons that will be hundreds or thousands of times more expensive than the savings of avoiding an uncontested lock.
If we are in the first case then figure out what is causing the contention and eliminate that. If you're doing a lot of waiting around on a lock then figure out why that is and replace the locking with a slim reader-writer-lock or restructure the application so that not so many threads are banging on the same lock at the same time.
In either case there is no justification for doing dangerous, implementation-sensitive low-lock techniques. You should only be using low-lock techniques in those incredibly rare cases where you really, truly cannot take the cost of an uncontested lock.
The reason I guess this question comes up again and again:
Why
Dictionary<TKey, TValue>
fails in the above case:I don't really think that you need to prove this, you just need to refer people to the documentation for
Dictionary<TKey, TValue>
:It's actually a well-known fact (or should be) that you cannot read from a dictionary while another thread is writing to it. I've seen a few "bizarre multi-threading issue" kinds of questions here on SO where it turned out that the author didn't realize that this wasn't safe.
The problem isn't specifically related to double-checked locking, it's just that the dictionary is not a thread-safe class, not even for a single-writer/single-reader scenario.
I'll go one step further and show you why, in Reflector, this isn't thread-safe:
Look at what can happen if the
Resize
method happens to be running while even one reader callsFindEntry
:And this is exactly what fails in dtb's example. Thread A searches for a key that is known in advance to be in the dictionary, and yet it isn't found. Why? Because the
FindValue
method picked what it thought was the correct bucket, but before it even had a chance to look inside, Thread B changed the buckets, and now Thread A is looking in some totally random bucket that does not contain or even lead to the right entry.Moral of the story:
TryGetValue
is not an atomic operation, andDictionary<TKey, TValue>
is not a thread-safe class. It's not just concurrent writes you need to worry about; you can't have concurrent read-writes either.In reality the problem actually runs a lot deeper than this, due to instruction reordering by the jitter and CPU, stale caches, etc. - there are no memory barriers whatsoever being used here - but this should prove beyond a doubt that there's an obvious race condition if you have an
Add
invocation running at the same time as aTryGetValue
invocation.Including the code in the question, you can test it with the following code.
This program just tries to get
Create
to traverse the collection as it is being "grown". It should be run on a machine with at least two cores (or two processors), and will most likely fail after a while with this exception.Adding this test is difficult since it is a probabilistic test, and you don't know how long it will take to fail (if ever). I guess you could pick a value like 10 seconds and let it run for that long. If it doesn't fail in that amount of time, then the test passes. Not the best, but something. You should also verify that
Environment.ProcessorCount > 1
before running the test, otherwise the likelihood of it failing is minuscule.