Race Condition Issues in Multithreaded Applications
Race conditions occur when two computer program processes, or threads, attempt to access the same resource at the same time and cause problems in the system. Race conditions are considered a common issue for multithreaded applications.
The situation where processes using shared memory do their own work at the wrong time and the data is corrupted is called a race condition. The main purpose is to ensure that the processes work in the correct order. For example, which process will be run first, which one will take the value there and how it will change are the best examples of race condition. Race, as the word suggests, is the competition of processes with each other. It is entirely our responsibility which process will run at what time.
Let’s check below example:
Synchronous
I wrote three methods. They count from 1 to 5, 5 to 10 and 10 to 15. I wrote “int” extension “To()” for more readability. I could use “Enumerable.Range()” function. We called all functions sequentially and got the results sequentially.
This To() extension is used for getting int[] array between spesific numbers.
GetOneToFiveNumbers();
GetFiveToTenNumbers();
GetTenToFifteenNumbers();
void GetOneToFiveNumbers()
{
1.To(5).ToList().ForEach(x => Console.WriteLine(x));
}
void GetFiveToTenNumbers()
{
5.To(10).ToList().ForEach(x => Console.WriteLine(x));
}
void GetTenToFifteenNumbers()
{
10.To(15).ToList().ForEach(x => Console.WriteLine(x));
}
static class Extensions
{
public static IEnumerable<int> To(this int from, int to)
{
int step = 1;
while (!(step > 0 ^ from < to) && from != to)
{
yield return from;
from += step;
}
}
}
Multithreading
Three functions are called at the sametime and got result as seen below. They are not sequentially. Why ? Firsty in C# Threads are managed by operating system. In multithread, every operations are running parallel at the same time. In reality, the processor is switching by using a scheduling algorithm. And also it’s switching based on a combination of external input parameters (interrupts) and how the threads have been prioritized. Thread counts depend on the number of cores in the CPU. Every functions work in diffirent processor core. At the end we are waiting for finishing all tasks and get result. So these three functions not wait each other. All works run at the same time.
/*GetOneToFiveNumbers();
GetFiveToTenNumbers();
GetTenToFifteenNumbers();*/
Task t1 = Task.Factory.StartNew(GetOneToFiveNumbers);
Task t2 = Task.Factory.StartNew(GetFiveToTenNumbers);
Task t3 = Task.Factory.StartNew(GetTenToFifteenNumbers);
Task[] taskList = new Task[] { t1, t2, t3 };
Task.WaitAll(taskList);
void GetOneToFiveNumbers()
{
1.To(5).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
void GetFiveToTenNumbers()
{
5.To(10).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
void GetTenToFifteenNumbers()
{
10.To(15).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
static class Extensions
{
public static IEnumerable<int> To(this int from, int to)
{
int step = 1;
while (!(step > 0 ^ from < to) && from != to)
{
yield return from;
from += step;
}
}
}
Result: Three functions results are not sequentially.
How to we handel these three functions for getting result Synchronous ?
“Past Present And Future Exist All At Once As Parallel Moments In Time.” ― Khalid Masood
1-) Solution Synchronized With Lock
As seen below, tasks related to a common “lock” object are waiting for each other. From this point of view, parallel work of the related tasks is prevented, but the situation where dependent processes crush each other is prevented.
/*GetOneToFiveNumbers();
GetFiveToTenNumbers();
GetTenToFifteenNumbers();*/
object locker = new object();
Task t1 = Task.Factory.StartNew(GetOneToFiveNumbers);
Task t2 = Task.Factory.StartNew(GetFiveToTenNumbers);
Task t3 = Task.Factory.StartNew(GetTenToFifteenNumbers);
Task[] taskList = new Task[] { t1, t2, t3 };
Task.WaitAll(taskList);
void GetOneToFiveNumbers()
{
lock (locker)
{
1.To(5).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
}
void GetFiveToTenNumbers()
{
lock (locker)
{
5.To(10).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
}
void GetTenToFifteenNumbers()
{
lock (locker)
{
10.To(15).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
}
}
static class Extensions
{
public static IEnumerable<int> To(this int from, int to)
{
int step = 1;
while (!(step > 0 ^ from < to) && from != to)
{
yield return from;
from += step;
}
}
}
If you have time you can check “Double Check Lock Object” at my below article. All the Tasks have ‘a Thread Pool. It is used for forcing the Tasks to run only one active thread at a time.
https://medium.com/swlh/async-lock-mechanism-on-asynchronous-programing-d43f15ad0b3
2-) Solution Synchronized With Task.ContinueWith()
Well, if it was wanted to start from a desired “Task” and to work synchronously, what would have to be done? This is where the “ContinueWith()” method comes into play. If the started thread ends, the new thread written in the method is executed.
/*GetOneToFiveNumbers();
GetFiveToTenNumbers();
GetTenToFifteenNumbers();*/
/*
object locker = new object();
Task t1 = Task.Factory.StartNew(GetOneToFiveNumbers);
Task t2 = Task.Factory.StartNew(GetFiveToTenNumbers);
Task t3 = Task.Factory.StartNew(GetTenToFifteenNumbers);
*/
Task t1 = Task.Factory.StartNew(GetOneToFiveNumbers);
Task t2 = t1.ContinueWith(antacedent => GetFiveToTenNumbers());
Task t3 = t2.ContinueWith(antacedent => GetTenToFifteenNumbers());
Task[] taskList = new Task[] { t1, t2, t3 };
Task.WaitAll(taskList);
void GetOneToFiveNumbers()
{
//lock (locker)
//{
1.To(5).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
//}
}
void GetFiveToTenNumbers()
{
//lock (locker)
//{
5.To(10).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
//}
}
void GetTenToFifteenNumbers()
{
//lock (locker)
//{
10.To(15).ToList().ForEach(x => { Task.Delay(1000); Console.WriteLine(x); });
//}
}
static class Extensions
{
public static IEnumerable<int> To(this int from, int to)
{
int step = 1;
while (!(step > 0 ^ from < to) && from != to)
{
yield return from;
from += step;
}
}
}
You can try “Thread.Join()”, “Monitor.Enter() and Monitor.Exit()” solutions too. In order not to prolong this article, I will not talk about these solutions further.
“What about if two threads try to use the same object at the same time?”
At the below we try to add same item array [ ] to the same Dictionary with the two threads at the same time.
Dictionary<int, string> dict = new();
AddKeyList().Wait();
dict.ToList().ForEach(entry => Console.WriteLine(entry.Key + ":" + entry.Value));
async Task AddKeyList()
{
var task1 = AddDict1();
var task2 = AddDict2();
await Task.WhenAll(task1, task2);
Console.WriteLine("All Dictionary Process Over!");
}
async Task AddDict1()
{
for (int x = 1; x < 30; x++)
{
await Task.Delay(100);
if (!dict.ContainsKey(x)) dict.Add(x, "AddDict1");
}
}
async Task AddDict2()
{
for (int x = 1; x < 30; x++)
{
await Task.Delay(100);
if (!dict.ContainsKey(x)) dict.Add(x, "AddDict2");
}
}
“A line has no parallel because it is itself the parallel.”
― RJ Clawson
Error:
We get an error. Because asynchronous methods do not wait for each other and after the “if (!dict.ContainsKey(x))” condition is entered in a thread, it is corrupted by another thread. An attempt is made to add the same element, which is already in the dictionary. You can see the Exception below.
Bussines Story: Situations like this can happen a lot in business life. And it can be so annoying when looking for a solution. For example, you are making an invoicing transaction with a sequential ID. You will assign 30 unique IDs that were previously reserved from a related collection to the printed invoices in order. Or 10 clients are waiting in line. You are making a chat or online game room. You will assign the next person asynchronously to the room that has been opened. In short, examples could be multiplied further.
Solution “ConcurrentDictionary”:
In asynchronous structures, “Dictionary” is not the right tool to use, as seen in the example above. For this, the “ConcurrentDictionary” collection would not be a wrong choice at all. Thus, it can be ensured that both threads do not overwhelm each other by checking with the “TryAdd
()” method. As can be seen below, the order of the dictinory elements can be changed. This is inherent in asynchronous programming. But the most important part is that no thread interferes with the work of the other and all the dictionary element’s key are only add once by different tasks and printed on the screen.
using System.Collections.Concurrent;
ConcurrentDictionary<int, string> dict = new();
AddKeyList().Wait();
dict.ToList().ForEach(entry => Console.WriteLine(entry.Key + ":" + entry.Value));
async Task AddKeyList()
{
var task1 = AddDict1();
var task2 = AddDict2();
await Task.WhenAll(task1, task2);
Console.WriteLine("All Dictionary Process Over!");
}
async Task AddDict1()
{
for (int x = 1; x < 30; x++)
{
await Task.Delay(100);
if (!dict.ContainsKey(x)) dict.TryAdd(x, "AddDict1");
}
}
async Task AddDict2()
{
for (int x = 1; x < 30; x++)
{
await Task.Delay(100);
if (!dict.ContainsKey(x)) dict.TryAdd(x, "AddDict2");
}
}
Result:
What about if two threads use same Queue ?
We will add ten items to the Queue. Later, we will run two tasks as parallel. These are get item from the Queue and print on the screen. Although we check the count of queues later, we will get “Queue Empty” error, while taking an item form Queue and print it to the screen.
using System.Collections;
Queue counter = new();
counter.Enqueue(1);
counter.Enqueue(2);
counter.Enqueue(3);
counter.Enqueue(4);
counter.Enqueue(5);
counter.Enqueue(6);
counter.Enqueue(7);
counter.Enqueue(8);
counter.Enqueue(9);
counter.Enqueue(10);
RunCount().Wait();
Console.ReadLine();
async Task CountItems(string taskName)
{
while (counter.Count > 0)
{
await Task.Delay(100);
Console.WriteLine($"{taskName}:{counter.Dequeue()}");
}
}
async Task RunCount()
{
var task1 = CountItems("Task1");
var task2 = CountItems("Task2");
await Task.WhenAll(task1, task2);
Console.WriteLine("Tüm işlem Bitti!");
}
“Multitasking is overrated — I’d rather do one thing well than many things badly. Quality supersedes quantity every time.” ― Stewart Stafford
Error:
We will get “Queue empty” error. Because asynchronous methods do not wait for each other and after the “while (counter.Count > 0)” condition is entered in a thread, it is corrupted by another thread. An attempt is made to get an element from an empty queue. You can see the relevant code below.
Solution “ConcurrentQueue<T>”:
In asynchronous structures, “Queue” is not the right tool to use, as seen in the example above. For this, the “ConcurrentQueue” collection would not be a wrong choice at all. Thus, it can be ensured that both threads do not overwhelm each other by checking with the “TryDequeue()” method.
using System.Collections;
using System.Collections.Concurrent;
ConcurrentQueue<int> counter = new();
counter.Enqueue(1);
counter.Enqueue(2);
counter.Enqueue(3);
counter.Enqueue(4);
counter.Enqueue(5);
counter.Enqueue(6);
counter.Enqueue(7);
counter.Enqueue(8);
counter.Enqueue(9);
counter.Enqueue(10);
RunCount().Wait();
Console.ReadLine();
async Task CountItems(string taskName)
{
while (counter.Count > 0)
{
await Task.Delay(100);
counter.TryDequeue(out int result);
Console.WriteLine($"{taskName}:{result}");
}
}
async Task RunCount()
{
var task1 = CountItems("Task1");
var task2 = CountItems("Task2");
await Task.WhenAll(task1, task2);
Console.WriteLine("Tüm işlem Bitti!");
}
Result:
As can be seen below, the order of the sort can be changed. First 2, then 1 can work. This is inherent in asynchronous programming. But the most important part is that no thread interferes with the work of the other and all the array elements are only taken once by different tasks and printed on the screen.
There are many more Thread Safe Collections in C#. Such as ConcurrentBag, ConcurrentStack.
The ConcurrentQueue<T> and ConcurrentStack<T> classes don’t use locks at all. Instead, they rely on Interlocked operations to achieve thread safety.
Conclusion:
It should not be forgotten that every good thing has a price. The downside to these beautiful Concurrent Collections is performance. In short, if it won’t work for you, ConcurrentDictionary should not be used instead of Dictionary or ConcurrentQueue should be used instead of Queue. As can be seen in the picture below, ConcurrentDictionary for a single thread should not be used for performance reasons. Because it consumes much more time and resources than a normal Dictionary.
“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”
Source:
- https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/
- https://stackoverflow.com/questions/915745/thoughts-on-foreach-with-enumerable-range-vs-traditional-for-loop
- https://www.baeldung.com/cs/async-vs-multi-threading
- https://www.dotnetperls.com/concurrentdictionary
- https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=netframework-4.7.2
- http://dotnetpattern.com/csharp-concurrentbag