Luna Tech

Tutorials For Dummies.

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.

Solution

  1. atomic operations
  2. 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:

  1. we need to be careful when adding a lock, as it may lead to deadlocks, especially when you’re using nested locks
  2. 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;
}