Using a thread-safe dictionary in .NET C# Part 3: thread-safe modifications
July 31, 2015 Leave a comment
In the previous post we looked at the 4 Try methods of ConcurrentDictionary that support CRUD operations: retrieval, deletion, update and insertion. We saw some basic examples for their usage and concluded that TryUpdate was not a very good solution to actually update an item due to race conditions.
This is where another method called AddOrUpdate enters the scene.
As the method name implies it can both update an existing value or add a new key-value pair if the key doesn’t exist yet. There are similar operations in the database world. It’s a mixture of UPDATE and INSERT called an UPSERT. AddOrUpdate is the ConcurrentDictionary equivalent of an UPSERT.
AddOrUpdate requires 3 parameters:
- The key
- The value if there’s no element by that key. In this case a new key-value pair will be added to the dictionary
- A function which will calculate the new value if an element by that key already exists. This is the update part of the operation
The update function – or delegate – in turn has the following requirements:
- Its signature must accept a key and a value whose types correspond to the key and value type of the ConcurrentDictionary
- It must return a value of the same type that the ConcurrentDictionary requires
If you’re new to delegates, functions and lambdas then they may look very cryptic at first. You can start your investigation here.
The update function provides a mechanism to calculate the new value based on the key and the existing value. It can be as simple or complex as you need. It always returns the value that was either added in case of a new item or the updated value if it’s an update.
Here’s an example of the AddOrUpdate method:
public void RunAddOrUpdateSample() { ConcurrentDictionary<string, int> movieCategoriesOnStock = new ConcurrentDictionary<string, int>(); movieCategoriesOnStock.TryAdd("Romance", 12); movieCategoriesOnStock.TryAdd("Action", 9); movieCategoriesOnStock.TryAdd("Comedy", 20); Debug.WriteLine("Dictionary content before AddOrUpdate:"); foreach (var kvp in movieCategoriesOnStock) { Debug.WriteLine(string.Format("{0}: {1}", kvp.Key, kvp.Value)); } int zombie = movieCategoriesOnStock.AddOrUpdate("Zombie", 10, (key, value) => { return value * 2; }); Debug.WriteLine("Zombie: {0}", zombie); int romance = movieCategoriesOnStock.AddOrUpdate("Romance", 5, (key, value) => { return CalculateNewValue(key, value); }); Debug.WriteLine("Romance: {0}", romance); Debug.WriteLine("Dictionary content after AddOrUpdate:"); foreach (var kvp in movieCategoriesOnStock) { Debug.WriteLine(string.Format("{0}: {1}", kvp.Key, kvp.Value)); } } private int CalculateNewValue(string key, int value) { return key.GetHashCode() + value * 3; }
This produces the following output:
Dictionary content before AddOrUpdate:
Action: 9
Romance: 12
Comedy: 20
Zombie: 10
Romance: -1978843422
Dictionary content after AddOrUpdate:
Action: 9
Romance: -1978843422
Zombie: 10
Comedy: 20
Romance got a crazy value but I just wanted to take an example where both the key and the value input parameters are used. Again, if the lambda functions look weird then you can consult the course referenced above.
View the list of posts on the Task Parallel Library here.