If you have worked in multi-threaded programs, you may have encountered these keywords – volatile, lock and Interlocked. But if you do not really understand what is the difference between these or which one is to be used when, this article might help.
Let us consider a scenario first. Let’s say there is an integer field defined in a class like below:
private int _counter = 0;
Suppose, that as per some business logic, you have to increment this counter. You can simply do it as:
_counter++;
Quiz time. If there are multiple threads that can access this logic, is there something wrong with the above statement? Think about it.
Okay, if you have answered that the above code will not behave as desired in a multi-threaded scenario, then you are absolutely correct. The above statement is in fact thread-unsafe. Why? Because the increment operation is not atomic. It means, that the operation can be preempted any time before it completes. When you run the program and create two threads that call this code, it may be possible that nothing goes wrong and everything works as expected. So, in the end, you will get the value of _counter variable as 2. However, in one of parallel universe, the following events may occur:
- Thread A reads the value of the variable as 0
- Thread B reads the value of the variable as 0
- Thread A performs the add operation and gets 1 as the result
- Thread B performs the add operation and gets 1 as the result
- Thread A writes the value 1 to _counter variable
- Thread B writes (overwrites) the value 1 to _counter variable
As you can see, a seemingly simple operation can behave unexpectedly under a multithreaded scenario. So, how we can solve the above issue?
Volatile Keyword
Let’s see if volatile would help. What happens when you declare the _counter variable as below:
private volatile int _counter = 0;
Will it solve the issue described above? The answer is NO. To understand why, let’s take a look at what volatile does. In normal circumstances, the CLR is free to perform some optimizations which can result in instruction re-ordering. It can also do the optimizations via CPU cache which prevents expensive trips to main memory. One side effect is that changes to a value stored in one CPU might not be immediately visible to thread running on another CPU. The volatile keyword prevents such optimizations from happening and instructs the CLR to always get the latest value from main memory instead of CPU cache. In this way, the thread never reads stale value.
However, if you consider our original problem, using volatile doesn’t help us at all. Because our problem is not of thread reading stale values. Our problem is that the instructions from two different threads are getting interleaved. Specifically, the read and write operations from different threads are getting intertwined due to lack of atomicity. Volatile keyword would have been helpful in a scenario where one thread is constantly writing to a field whereas another thread is constantly reading it.
So, what can we do to solve our problem?
lock keyword
If you wrap the increment statement around a lock block, what would happen?
lock(_object)
{
_counter++;
}
Using lock would prevent the unwanted behavior and our problem should be solved. This is because anything inside the lock statement is only accessible by one thread at a time. So, if Thread A executes that lock statement first, the critical section (inside lock) cannot be executed by Thread B until Thread A is finished executing it. Thread B will be blocked if its execution reaches the lock statement while Thread A is in the middle of executing it.
Quiz time. Is the block inside the lock statement now atomic? Think…
If you answered yes, it is incorrect. This is because the thread might still be preempted while it is in the middle of the critical section. In that case, the lock will be held longer, that’s all. The critical section is still divisible and as such non-atomic. An important implication of this deduction is that if the variable _counter is modified elsewhere in the code, it will again result in unexpected behavior. So, you will need to wrap all such statements under the lock using the same object.
Using locks blatantly in the code everywhere is equally bad though. Lock comes with a performance hit and in a latency-sensitive application, it may easily become a bottleneck. Additionally, using too many locks on the same object may result in excessive blocking than required and in the worse case can lead to deadlock. So, use lock keyword with caution and utmost care.
Interlocked class
Finally, we can further improve our code by using Interlocked class instead of locks. The Interlocked class provides atomic operations for variables that are shared by multiple threads. One of the methods is Increment(). We can write our code like this:
Interlocked.Increment(ref _counter);
The above statement is atomic and thus cannot be pre-empted. It is also much faster than locks. So, in scenarios like above, it is the best way to achieve atomicity in a multithreaded environment.
The best explanation I have seen
Nicely written. Also worth mentioning that lock keyword is nothing but Monitor.Enter() & Monitor.Exit() behind the scenes
Thank you. Yes, you are right about the use of the Monitor class. I didn’t mention it so as to keep the topic on point. Nevertheless, good detail to be aware of.
I am from java background, and it looks the concept is almost the same there as well. We do not have Interlocked though. An equivalent of that would be AtomicInteger I guess.
Yes, the concepts are strikingly similar.