Race Condition
2022-02-20
Race Condition
Be careful when sharing variables and resouces in a parallel process as you will run into race conditions.
- we need to make sure only one thread at a time is changing the shared variable
- we need to use something that allows us to change the value in a thread-safe manner.
Solution
- atomic operations
- lock
Note: Always perfer atomic operations over lock when possible as its less overhead and performs faster. If not possible, we need to introduce a locking mechanism.
How to use atomic operations with the interlock class?
Suitable for: operations on an integer to increment, decrement or add.
Tool: System.Threading has a class called interlocked, we can use it to perform atomic operations on 32-bit and 64-bit integers.
Note: Interlocked uses less instructions and is faster than a lock.
void Main()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
int total = 0;
Parallel.For(0, 100, (i) =>
{
var result = Compute(i);
Interlocked.Add(ref total, result);
});
Console.WriteLine(total);
Console.WriteLine($"It took: {stopwatch.ElapsedMilliseconds}ms to run");
}
static Random random = new Random();
static int Compute(int value)
{
var randomMilliseconds = random.Next(10, 50);
var end = DateTime.Now + TimeSpan.FromMilliseconds(randomMilliseconds);
while (DateTime.Now < end) { }
return value + 100;
}
How to introduce a lock?
What is a deadlock and how to avoid it from happening.
We can introduce a lock object, only one thread at a time can run the code inside the lock statement.
If we cannot use interlocked because of data type, then we have to use lock.
Considerations:
- we need to be careful when adding a lock, as it may lead to deadlocks, especially when you’re using nested locks
- only lock for as short of a time as possible, do as little work as possible in the lock statement
- calling an expensive operation inside a lock is not recommended as it forces other threads to wait
void Main()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
decimal total = 0;
// non-parallel version
// for(int i = 0; i < 100; i++){
// total += Compute(i);
// }
// This is slow as the computation happens inside the lock
// Parallel.For(0, 100, (i) => {
// lock (syncRoot){
// total += Compute(i);
// }
// });
// This is faster as the computation happens outside the lock
Parallel.For(0, 100, (i) => {
var result = Compute(i);
lock (syncRoot){ // the lock time should be short!
total += result;
}
});
Console.WriteLine(total);
Console.WriteLine($"It took: {stopwatch.ElapsedMilliseconds}ms to run");
}
static object syncRoot = new object();
static Random random = new Random();
static decimal Compute (int value) {
var randomMilliseconds = random.Next(10, 50);
var end = DateTime.Now + TimeSpan.FromMilliseconds(randomMilliseconds);
while (DateTime.Now < end) {}
return value + 0.5m;
}