You might be wondering what is race condition or do you even need to care of what it does to your application? Well yes, you do in fact need to know what it is as it may lead to your application not performing what you intended it to be.
A race condition is a situation where 2 or more processes / threads is accessing the same resource at the same time, in which lead to undesired result to occur.
Let's use the following code to illustrate how race condition occurred. Assume that you want to increase the _value by 1 each time the IncreaseValue() is called.
[C#]
private static int _value = 0;
private static int GetValue()
{
return _value;
}
private static int SetValue(int value)
{
value += 1;
return value;
}
public void IncreaseValue()
{
var value = GetValue();
_value = SetValue(value);
}
[VB]
Private Shared _value As Integer = 0
Private Function GetValue() As Integer
Return _value
End Function
Private Function SetValue(ByVal value As Integer) As Integer
value = value + 1
Return value
End Function
Public Sub IncreaseValue()
Dim value As Integer = GetValue()
_value = SetValue(value)
End Sub
The following table shows how the code execute if you are running the above code in a single thread environment. In this case, race condition will not occur.
Step
|
Thread 1
|
Value in _value
|
1
|
Call IncreaseValue() method
|
0
|
2
|
Call GetValue() and value obtained is 0
|
0
|
3
|
Call SetValue() and value obtained is 1 and assign 1 to _value
|
1
|
4
|
Exit IncreaseValue() method.
|
1
|
5
|
Call IncreaseValue() method.
|
1
|
6
|
Call GetValue() and value obtained is 1
|
1
|
7
|
Call SetValue() and value obtained is 2 and assign 2 to _value
|
2
|
8
|
Exit IncreaseValue() method.
|
2
|
But imagine that there are more than 1 threads accessing the static variable _value at the same time.
Step
|
Thread 1
|
Step
|
Thread 2
|
Value in _value
|
1
|
Call IncreraseValue() method
|
2
|
Call IncreaseValue() method
|
0
|
3
|
Call GetValue() and value obtained is 0
|
4
|
Call GetValue() and value obtained is 0
|
0
|
5
|
Call SetValue() and value obtained is 1 and assign 1 to _value
|
6
|
Call SetValue() and value obtained is 1 and assign 1 to _value
|
1
|
7
|
Exit IncreaseValue() method
|
8
|
Exit IncreaseValue() method
|
1
|
The final result supposed to be 2, but due to the original value obtained by both of the thread is 0, both of them will end up having the same final value of 1 and assign it back to _value. So this is how race condition occurred.
To handle the race condition, you can rely on any one of the synchronization primitives as shown here
https://msdn.microsoft.com/en-us/library/ms228964(v=vs.110).aspx.
You may refer to my previous blog posts on how to use each of the synchronization primitives.
Mutex:
http://jaryl-lan.blogspot.com/2015/08/thread-synchronization-with-mutex.html
SpinLock:
http://jaryl-lan.blogspot.com/2015/08/thread-synchronization-with-spinlock.html
Semaphore:
http://jaryl-lan.blogspot.com/2015/11/thread-synchronization-with-semaphore.html
For this example, we will use lock keyword.
[C#]
private static int _value = 0;
private object _lock = new object();
private static int GetValue()
{
return _value;
}
private static int SetValue(int value)
{
value += 1;
return value;
}
public void IncreaseValue()
{
lock (_lock)
{
var value = GetValue();
_value = SetValue(value);
}
}
[VB]
Private Shared _value As Integer = 0
Private _lock As Object = New Object()
Private Function GetValue() As Integer
Return _value
End Function
Private Function SetValue(ByVal value As Integer) As Integer
value = value + 1
Return value
End Function
Public Sub IncreaseValue()
SyncLock _lock
Dim value As Integer = GetValue()
_value =
SetValue(value)
End SyncLock
End Sub
At any one time, only 1 thread can access the code in lock statement. So the subsequent threads have to wait for the current thread in the lock statement to complete its execution and exit from lock statement block. Thus, race condition did not occur. The following table shows how the code executes with lock keyword.
Step
|
Thread 1
|
Step
|
Thread 2
|
Value in _value
|
1
|
Call IncreraseValue() method
|
2
|
Call IncreaseValue() method
|
0
|
3
|
Execute lock keyword
|
4
|
Execute lock keyword.
|
0
|
5
|
Call GetValue() and value obtained is 0
|
|
|
0
|
6
|
Call SetValue() and value obtained is 1 and assign 1 to _value
|
|
|
1
|
7
|
Exit from lock statement
|
8
|
Call GetValue() and value obtained is 1
|
1
|
9
|
Exit IncreaseValue() method
|
10
|
Call SetValue() and value obtained is 2 and assign 2 to _value
|
2
|
|
|
11
|
Exit from lock statement
|
2
|
|
|
12
|
Exit IncreaseValue() method
|
2
|
It is not a must to use synchronization primitives to handle race condition. There are many other ways to handle race condition based on different kind of situations or scenarios.
So for example if you want to assign a value to a variable or instantiate an object and assign it to a variable if it is null, you can rely on lazy loading or static initialization.
If is SQL Server related, you can rely on one of the following.
Hints:
https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table
sp_getapplock:
https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql
sp_releaseapplock:
https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-releaseapplock-transact-sql
You may also be in a situation where you just need to revise your application design without relying on the above listed technologies to handle race condition.