In multithreaded applications, multiple threads often access shared resources such as variables, collections, or files. If this access is not controlled properly, it can lead to inconsistent or unexpected results, a problem known as a race condition. To avoid this, developers write thread-safe code so that shared resources behave predictably even under concurrent access.
What is a Race Condition?
A race condition occurs when two or more threads try to access and modify a shared resource simultaneously without proper synchronization. The outcome depends on the order in which threads execute, which makes the programâs behavior unpredictable.
Example:
using System;
using System.Threading;
class Counter
{
private int count = 0; // Shared resource
public void Increment()
{
for (int i = 0; i < 1000; i++)
{
count++; // Not thread-safe
}
}
public int GetValue() => count;
}
class Geeks
{
static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(counter.Increment);
Thread t2 = new Thread(counter.Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Final Count: " + counter.GetValue());
}
}
Possible Outputs:
Final Count: 1567
or
Final Count: 2000
Explanation:
- The count++ operation is not atomic, consisting of multiple steps (read, increment, write).
- If two threads execute simultaneously, one threadâs update may overwrite the other, producing inconsistent results.
What is Thread Safety?
Thread safety means that shared data can be accessed by multiple threads without causing inconsistent results. A thread-safe code ensures predictable behavior regardless of how many threads execute it concurrently.
- Thread-safe code prevents race conditions.
- It guarantees consistent results regardless of thread scheduling.
- Thread safety can be achieved through atomic operations, immutable data, or concurrent collections.
Immutable objects are inherently thread-safe because their state cannot change after creation, eliminating race conditions.
Techniques to Achieve Thread Safety
C# provides multiple ways to make code thread-safe and avoid race conditions.
1. Using lock Keyword
using System;
using System.Threading;
class Counter
{
private int count = 0;
private readonly object locker = new object();
public void Increment()
{
for (int i = 0; i < 1000; i++)
{
lock (locker) // Ensures one thread at a time
{
count++;
}
}
}
public int GetValue() => count;
}
class Geeks
{
static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(counter.Increment);
Thread t2 = new Thread(counter.Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Final Count: " + counter.GetValue());
}
}
Output
Final Count: 2000
The
lockstatement ensures that only one thread executes the critical section at a time, preventing race conditions.
2. Using Interlocked Class
The Interlocked class provides atomic operations for thread-safe updates without using locks.
using System;
using System.Threading;
class Counter
{
private int count = 0;
public void Increment()
{
for (int i = 0; i < 1000; i++)
{
Interlocked.Increment(ref count); // Thread-safe increment
}
}
public int GetValue() => count;
}
class Geeks
{
static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(counter.Increment);
Thread t2 = new Thread(counter.Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Final Count: " + counter.GetValue());
}
}
Output
Final Count: 2000
3. Using Mutex
Mutex is used when synchronization is needed across multiple processes as well as threads.
using System;
using System.Threading;
class Counter
{
private static Mutex mutex = new Mutex();
private int count = 0;
public void Increment()
{
for (int i = 0; i < 1000; i++)
{
mutex.WaitOne();
try
{
count++;
}
finally{
mutex.ReleaseMutex();
}
}
}
public int GetValue() => count;
}
class Geeks
{
static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(counter.Increment);
Thread t2 = new Thread(counter.Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Final Count: " + counter.GetValue());
}
}
Output
Final Count: 2000
4. Using Thread-Safe Collection
Concurrent collections (like ConcurrentDictionary or ConcurrentQueue) provide built-in thread safety and reduce the need for explicit synchronization.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Geeks
{
static void Main()
{
var queue = new ConcurrentQueue<int>();
Parallel.For(0, 1000, i => queue.Enqueue(i)); // Thread-safe enqueue
Console.WriteLine("Items in queue: " + queue.Count);
}
}
Output
Items in queue: 1000