I don't really think that you need to prove this, you just need to refer people to the documentation for Dictionary<TKey, TValue>
:
A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.
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:
private int FindEntry(TKey key)
{
// Snip a bunch of code
for (int i = this.buckets[num % this.buckets.Length]; i >= 0;
i = this.entries[i].next)
// Snip a bunch more code
}
private void Resize()
{
int prime = HashHelpers.GetPrime(this.count * 2);
int[] numArray = new int[prime];
// Snip a whole lot of code
this.buckets = numArray;
}
Look at what can happen if the Resize
method happens to be running while even one reader calls FindEntry
:
- Thread A: Adds an element, resulting in a dynamic resize;
- Thread B: Calculates the bucket offset as (hash code % bucket count);
- Thread A: Changes the buckets to have a different (prime) size;
- Thread B: Chooses an element index from the new bucket array at the old bucket index;
- Thread B's pointer is no longer valid.
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, and Dictionary<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 a TryGetValue
invocation.