Identifying and solving thread races can be a frustrating experience, depending on the complexity of the software. That’s why it is important to recognize code that is shared among threads and protect it properly. One way of achieving that is by using thread-safe collections, which are classes ready to deal with multithreading scenarios. However, when not properly used, it won’t help you as you expect.
The test code
In the example below, our objective is to add a hundred items to the dictionary, while also adding them to this “itemsAddedToTheDictionary”. In order to finish this quicker, we are running the works in several threads, because the more the merrier right? Then when the work is done, we print the number of items that we have on the list, which must be the same as the number of items in the dictionary.
Let’s take a look at the UpdateDictionaryAndList method, which as the name clearly suggests, updates our dictionary and the list. We can see that for every iteration, it checks if the dictionary contains a certain key, and, in case it doesn’t, calls the ConcurrentDictionary AddOrUpdate method. And finally, we add it to our “itemsAddedToTheDictionary” list. We are also adding a Thread.Sleep of 10ms just to cause a bit of chaos, as we don’t want the threads doing their work almost immediately.
Note: for the sake of example, the AddOrUpdate method is purposedly used in the wrong way here. When using it in a real-world situation, you should specify a proper update function.
Let’s run the code and see what’s our result:
Total of items on the dictionary: 100
Total of created objects: 289
Doesn’t look good right? The reason is that we are checking if the key exists, and then only on the next statement we are updating the dictionary. That causes the possibility of 2 or more threads checking at the same time if the key exists, and then creating and adding objects. The dictionary is not affected by this problem, because the AddOrUpdate method prevents that by updating the existing item with the specified update function. Our list, on the other hand, just adds whatever we provide to it.
The solution
To fix this situation, we need a different approach to how we are using our ConcurrentDictionary. Instead of checking if the key exists and modifying the dictionary, we can use the TryAdd method, which will only add an item if the key is not contained in the dictionary. In case a new item was added, it returns true, a value that we can use to decide if we should or not include the item in our list.
Total of items on the dictionary: 100
Total of created objects: 100
Conclusion
When creating thread-safe code, you need to be mindful of every statement. There are several tools for helping you achieve thread safety, such as thread-safe collections, locks, semaphores, etc. But whatever the situation, it’s recommended that you research the tool you are about to use, so then you can use it properly and avoid unpleasant surprises in the future.