Understanding of ConcurrentDictionary in .NET
Rasul
Posted on May 8, 2024
Everything is becoming more concurrent, so usually server-based(multithreaded environments) applications are faced with non-thread-safe problems such as race condition.
Especially when we are using the collections every .NET developer knows that BCL (Base Class Library) has a key-value pairs collection whose name is Dictionary. However, the problem is Dictionary is not thread-safe. So, it means we can face non-thread-safe problems. To eliminate these problems we are using the ConcurrentDictionary which provides thread safety and does the same things (in general).
Let’s look at one of the most common causes of problems!
Race Condition Problem
I will explain the race condition problem with Dictionary.
So, let’s think that we have a method that checks the argument (key) with the Dictionary ContainsKey method. And if the result is false, then it adds that key and value (arguments) to the dictionary instance. Otherwise the code block will do nothing.
The above explanation is illustrated in the image and also shows the runtime steps of 2 threads, too. So, in the image we have multithread environment that consists of 2 threads. And it means two different threads are reading same code block at the same time (at the same time is an illusion, in fact they do not work at the same time but this is a topic for another article).
Let’s look at the image step by step. Thread1
executes the first line and gets a false result. Then, the context switches to Thread2
; it executes the first line and gets false. So, Thread1 and Thread2
enter the if block. Here is the problem, when the context switches to Thread1
, it will add a key and value to the dictionary. After that, the context switches to Thread2
, which will try to add a key and value to the dictionary, too. But in our case, Thread2
will get an error because the key in the dictionary should be unique.
This is what we call the Race Condition problem because in multithread environments the threads are always racing between each other.
Note: There are different types of race condition problems; this example is one of them.
ConcurrentDictionary
ConcurrentDictionary is the generic implementation of a hash-table data structure that is used to store key/value pairs. And also it is providing thread-safety for the using in multithreading scenarios.
For constructing the ConcurrentDictionary, it has 2 items:
- сapacity: It is the initial size of the bucket which stores key/value pairs.
- concurrencyLevel: It defines the count of threads that will work on the given data structure at the same time. For example, if you have specified concurrentLevel = 2, then the rest of the threads will wait for these 2 threads to finish (a least one of them must be released). So, it helps us to configure the estimated number of threads that will update the items (key/value pairs) concurrently.
In general, implementations of concurrent data structures have 3 techniques:
- Lock-free: The operations can proceed concurrently without the use of traditional locks.
- Fine-grained: It divides data structure into smaller units and block them individually. It means, when you need to manipulate exact data that other thread can reach, blocking will happen in a unit level not an entire data structure.
- Transanctional Memory: abstracts concurrent operations into transactions, which are executed atomically and isolated from each other.
The ConcurrentDictionary uses a fine-grained technique for getting better performance in the case of a multithread environment.
Methods
We can group the methods in terms of implementation:
- Non-blocking functionalities: GetEnumerator, this[], TryGet, ContainsKey
- Blocking items by individualy functionalities: TryAdd, TryUpdate, TryRemove
- Blocking all dictionary items (non-effective one): Count, IsEmpty, Keys, Values, CopyTo, Clear, ToArray
Let’s continue by writing an example. The basic code below has solve race condition problem (mentioned above) by ConcurrentDictionary.
public class Program
{
public static ConcurrentDictionary<int, string> dictionaryInstance =
new ConcurrentDictionary<int, string>();
public static void Main()
{
for (int i = 0; i < 10; i++)
{
string valueStr = $"Task{i + 1}";
ThreadPool.QueueUserWorkItem((s) => ThreadSafeAddValue(15, valueStr)); // Just for testing purpose
}
Console.Read();
}
static void ThreadSafeAddValue(int key, string value)
{
dictionaryInstance.TryAdd(key, value);
}
}
Benefits
As you can see the ConcurrentDictionary provides many benefits like:
- Thread safety with optimization, because it increases the performance of operations. For that reason, it is better than manual/customized locking/critical section technique (Monitor, lock etc..).
- It uses lightweight synchronization (SpinWait, SpinLock) that uses spinning before putting threads to wait; for short wait periods, spinning is less expensive than waiting which involves kernel switching.
- Gives already tested key/value pairs functionalities which ready to be used (for all lazy developers 😊).
As you can see the ConcurrentDictionary would be the best choice instead of using Dictionary in case of a multithread environment.
Posted on May 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.