C# – Add or overwrite a value in ConcurrentDictionary

The simplest way to add or overwrite a value in a ConcurrentDictionary is to use the indexer:

var movieMap = new ConcurrentDictionary<int, Movie>();

//add
movieMap[123] = new Movie();

//overwrite
movieMap[123] = new Movie();
Code language: C# (cs)

If the key doesn’t exist, this adds it. If the key exists, this overwrites it. The indexer is thread-safe.

The indexer is the simplest way to unconditionally add / overwrite a value. Sometimes you’ll want to use other ConcurrentDictionary methods for adding/updating values depending on your scenario.

In this article, I’ll show examples of using TryAdd() and AddOrUpdate(), and explain when to use them instead of using the indexer.

When to use TryAdd()

TryAdd() adds a key/value pair if the key doesn’t exist already, and returns true if was able to add it. This is useful when you don’t want to overwrite an existing key, and if you want to know if there was an existing key.

Here’s an example of using TryAdd():

if (!sessionMap.TryAdd(sessionId, new Session()))
{
	throw new SessionExistsException();
}
Code language: C# (cs)

Compare this with the following thread-unsafe code:

if (!sessionMap.ContainsKey(sessionId))
{
	sessionMap[sessionId] = new Session();
}
else
{
	throw new SessionExistsException();
}
Code language: C# (cs)

This is thread-unsafe because it has a race condition. Thread B could insert a key/value pair right after ContainsKey() returns false for Thread A. Hence, Thread A would incorrectly overwrite the key/value pair added by Thread B.

TryAdd() makes this operation atomic and therefore thread-safe.

When to use AddOrUpdate()

If the key doesn’t exist, AddOrUpdate() adds it. If the key exists, AddOrUpdate() overwrites it with the value returned by the passed in updateValueFactory delegate. It passes the current value to the delegate, which enables you to calculate a new value based on the current value.

In other words, if you want to update existing keys based on the current value, use AddOrUpdate(). If you want to just overwrite the existing keys, use the indexer.

Here’s an example of using AddOrUpdate(). Let’s say you’re using multiple threads to read different files and count words, and all of the threads are updating the shared ConcurrentDictionary. Here’s how to call AddOrUpdate():

wordCountMap.AddOrUpdate(word, addValue: 1, 
	updateValueFactory: (key, currentValue) => currentValue + 1);
Code language: C# (cs)

If the key doesn’t exist, it sets the value to the addValue parameter. If the key exists, it calls the passed in updateValueFactory delegate to get the new value.

Warning: updateValueFactory can run repeatedly

When there are multiple threads calling AddOrUpdate() concurrently, it’s possible for updateValueFactory to run repeatedly.

Here’s an example showing updateValueFactory running repeatedly. This is calling AddOrUpdate() concurrently, incrementing the value by 1:

var wordMap = new ConcurrentDictionary<string, int>();
wordMap.TryAdd("code", 0);

var allTasks = new List<Task>();

for (int i = 0; i < 10; i++)
{
	int taskId = i;   
	allTasks.Add(Task.Run(() =>
	{
		wordMap.AddOrUpdate("code", 0, updateValueFactory: (key, currentValue) =>
		{
			Console.WriteLine($"taskid={taskId} currentValue={currentValue}");

			return currentValue + 1;
		});

	}));
}
await Task.WhenAll(allTasks);
Console.WriteLine($"Final value={wordMap["code"]}");
Code language: C# (cs)

This outputs the following.

taskid=2 currentValue=0
taskid=6 currentValue=0
taskid=1 currentValue=0
taskid=7 currentValue=0
taskid=4 currentValue=0
taskid=0 currentValue=0
taskid=5 currentValue=0
taskid=3 currentValue=0
taskid=0 currentValue=1
taskid=7 currentValue=1
taskid=7 currentValue=2
taskid=6 currentValue=1
taskid=6 currentValue=3
taskid=1 currentValue=1
taskid=1 currentValue=4
taskid=8 currentValue=2
taskid=8 currentValue=5
taskid=2 currentValue=1
taskid=2 currentValue=6
taskid=3 currentValue=1
taskid=3 currentValue=7
taskid=5 currentValue=1
taskid=5 currentValue=8
taskid=9 currentValue=2
taskid=9 currentValue=9
Final value=10Code language: plaintext (plaintext)

Notice the updateValueFactory lambda executed 25 times. At the beginning, it executed 8 times concurrently (all of the lines have currentValue=0). At the end, you can see the final value is 10, which is correct.

This happens because AddOrUpdate() tries to insert/update in a loop until it succeeds. Every time it attempts to update, it has to call updateValueFactory again (because the current value could’ve changed since the previous attempt).

Furthermore, updateValueFactory isn’t executed within a lock (so make sure the lambda you pass in is thread-safe).

This problem happens for all overloads of AddOrUpdate() (and GetOrAdd()) that have delegate parameters. The problem isn’t specific to updateValueFactory.

If you must use these methods, be aware of these problems with the delegates. If possible, use the indexer to add / overwrite values instead.

Leave a Comment