Is ConcurrentDictionary Keys or Values property threadsafe
Have a question regarding thread safety with ConcurrentDictionary
. From the API, I see that the enumerator is thread-safe, but I don't see the same for keys and values properties. My question is:
Is it safe to loop over the Keys
or Values
collection when there are other threads modifying it concurrently?
While I do like the documentation, I tend to verify things with a small program when in doubt or I feel that I might be assuming too much.
The following code verifies that indeed you can enumerate the values collection safely while adding or removing keys from a separate thread to that on which the enumeration is taking place. This will not cause the usual collection was modified exceptions. In more detail, here are a couple of test cases
Case 1: Enumerating Values and deleting a key
If you follow the following sequence:
- start enumerating the values collection from a thread
- remove a key from a different thread that we have not enumerated yet
- Continue enumerating on the original thread
The observed behavior is that the removed key will indeed be enumerated since it existed in the values collection when we started the enumeration. No exception will be raised.
Case 2: Enumerating Values and adding a key
- start enumerating the values collection from a thread
- add a new key from a different thread that we have not enumerated yet
- Continue enumerating on the original thread
The observed behavior is that the added key will not be enumerated since it did not exist in values collection when we started to enumerate it. No exception will be raised whether we use TryAdd or add by assigning directly to the dictionary ie dictionary[key] = value.
Sample Code
Here is the sample program that demonstrates both cases:
ConcurrentDictionary<int, int> dictionary = new ConcurrentDictionary<int, int>();
// Seed the dictionary with some arbitrary values;
for (int i = 0; i < 30; i++)
{
dictionary.TryAdd(i, i);
}
// Reader thread - Enumerate the Values collection
Task.Factory.StartNew(
() =>
{
foreach (var item in dictionary.Values)
{
Console.WriteLine("Item {0}: count: {1}", item, dictionary.Count);
Thread.Sleep(20);
}
}
);
// writer thread - Modify dictionary by adding new items and removing existing ones from the end
Task.Factory.StartNew(
() =>
{
for (int i = 29; i >= 0; i--)
{
Thread.Sleep(10);
//Remove an existing entry
int removedValue;
if (dictionary.TryRemove(i, out removedValue))
Console.WriteLine("Removed item {0}", removedValue);
else
Console.WriteLine("Did not remove item {0}", i);
int iVal = 50 + i*2;
dictionary[iVal] = iVal;
Thread.Sleep(10);
iVal++;
dictionary.TryAdd(iVal, iVal);
}
}
);
Console.ReadKey();
And here is the output in release mode:
ConcurrentDictionary Represents a thread-safe collection of key-value pairs that can be accessed by multiple threads concurrently.
Source: MSDN
Yes, its threadsafe. But, even though it is threadsafe you should prefer NOT to to use Keys
, Values
, and also Count
.
Especially so when you are using ConcurrentCollections<T>
because you want to minimize lock contention, blocking of threads, and allocation of memory. And you do want those things if you care about performance and efficiency.
Look at the reference source to see why - Keys
immediately calls a GetKeys()
helper and that takes every single lock before continuing. Once it has the locks, it copies every single key into a new List<TKey>
, and returns a readonly view of that -- just so someone cannot accidentally mutate what is really just a temporary copy of the keys collection. This requires allocating quite large arrays, and holding the locks for quite some time, if your collection got large!
Values
locks and copies every single value similarly to Keys
. Even Count
acquires all locks, not in order to copy, in order to sum up all the internal table segment lengths. All just to get a momentarily 'consistent' count of objects in the collection, which is useful only as a rough estimate or historical footnote once the lock is released.
So yes, sigh, if you need atomic consistency I suppose this may be the price you have to pay. But! Perhaps you are luckier than that. You then might realize consistency is not actually required for your scenario, and that much more performant APIs to those bad ones are within your grasp - like using GetEnumerator()
to get an approximate idea of what items are in your collection instead! The remark in GetEnumerator()
's docs:
The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary, however it does not represent a moment-in-time snapshot of the dictionary. The contents exposed through the enumerator may contain modifications made to the dictionary after GetEnumerator was called.
In other words, it doesn't need to lock or copy at all because it doesn't need to ensure consistency. Hooray!