C# Thread Synchronization, Thread Safety, and Parallel Programming
1. What is the lock keyword in C#?
What is it?
The lock keyword is used to prevent multiple threads from executing a critical section of code simultaneously using the same lock object.
It helps prevent race conditions when accessing shared data.
Example
int counter = 0;
object lockObj = new object();
Parallel.For(0, 1000, i =>
{
lock (lockObj)
{
counter++;
}
});
Console.WriteLine(counter);
Output:
1000
Explanation:
-
lockObjis the synchronization object. -
Only one thread can enter the protected block at a time.
-
Other threads wait until the lock is released.
-
counter++executes safely.
Flow Diagram
Thread 1 ──> Acquire Lock ──> counter++
|
v
Release Lock
|
Thread 2 ──> Acquire Lock ──> counter++
Key Points
-
lockprevents simultaneous access to a critical section. -
It protects shared mutable data.
-
The lock is automatically released when the block exits, even if an exception occurs.
-
Prefer a dedicated private lock object.
-
You cannot use
awaitinside alockblock.
2. What is the difference between lock, Monitor, Mutex, and SemaphoreSlim?
What is it?
All four mechanisms control access to shared resources, but they have different capabilities.
|
Feature |
|
|
|
|
|---|---|---|---|---|
|
Purpose |
Mutual exclusion |
Mutual exclusion |
Mutual exclusion |
Limit concurrent access |
|
Scope |
Same process |
Same process |
Same or different processes |
Same process |
|
Multiple concurrent entries |
No |
No |
No |
Yes, configurable |
|
Timeout support |
No direct timeout |
Yes |
Yes |
Yes |
|
Async waiting |
No |
No |
No |
Yes |
|
Typical usage |
Shared variables |
Advanced synchronization |
Cross-process locking |
Async resource control |
Example: SemaphoreSlim
SemaphoreSlim semaphore = new SemaphoreSlim(2);
async Task ProcessAsync(int id)
{
await semaphore.WaitAsync();
try
{
Console.WriteLine($"Task {id} started");
await Task.Delay(1000);
}
finally
{
semaphore.Release();
}
}
await Task.WhenAll(
ProcessAsync(1),
ProcessAsync(2),
ProcessAsync(3)
);
Explanation:
-
SemaphoreSlim(2)allows a maximum of two concurrent entries. -
Tasks 1 and 2 can enter immediately.
-
Task 3 waits until a permit is released.
-
WaitAsync()supports asynchronous waiting.
Diagram
SemaphoreSlim(2)
|
+---- Task 1 → Running
|
+---- Task 2 → Running
|
+---- Task 3 → Waiting
|
Permit Released
|
v
Running
Key Points
-
Use
lockfor simple synchronous critical sections. -
Use
Monitorwhen advanced locking features such asTryEnter,Wait, andPulseare needed. -
Use
Mutexwhen synchronization between processes is required. -
Use
SemaphoreSlimfor asynchronous synchronization or limiting concurrent operations. -
lockandMonitorare reentrant for the owning thread;SemaphoreSlimis not thread-owner-aware.
3. What is Thread Safety?
What is it?
Thread safety means code behaves correctly when multiple threads access it concurrently.
It prevents inconsistent results, corrupted data, and race conditions.
Example
class Counter
{
private int _count = 0;
private readonly object _lock = new();
public void Increment()
{
lock (_lock)
{
_count++;
}
}
public int GetCount()
{
lock (_lock)
{
return _count;
}
}
}
Explanation:
-
Multiple threads may call
Increment(). -
The
lockensures only one thread modifies_countat a time. -
GetCount()also uses the lock to read the shared value consistently.
Key Points
-
Thread safety is important when data is shared across threads.
-
Local variables are generally independent between method invocations.
-
Shared static fields and singleton services require special attention.
-
Use
lock,Interlocked, immutable data, or concurrent collections when appropriate. -
Thread-safe code does not necessarily execute faster.
4. What is the difference between Parallel.ForEach() and a normal foreach?
What is it?
-
foreach: Processes items sequentially. -
Parallel.ForEach(): Processes items concurrently, potentially on multiple CPU cores.
Example
int[] numbers = { 1, 2, 3, 4, 5 };
// Normal foreach
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// Parallel.ForEach
Parallel.ForEach(numbers, number =>
{
Console.WriteLine(number);
});
Explanation:
Normal foreach:
1 → 2 → 3 → 4 → 5
Parallel.ForEach():
Thread 1 → 1, 4
Thread 2 → 2, 5
Thread 3 → 3
Execution order is not guaranteed.
Comparison
|
|
|
|---|---|
|
Sequential execution |
Parallel execution |
|
Preserves iteration order |
Execution order not guaranteed |
|
Minimal overhead |
Scheduling overhead |
|
Suitable for simple operations |
Suitable for CPU-intensive independent operations |
|
Easier debugging |
Requires concurrency awareness |
Key Points
-
Parallel.ForEach()is useful for CPU-bound workloads. -
It uses ThreadPool threads.
-
Parallel processing does not always improve performance.
-
Avoid modifying shared variables without synchronization.
-
For asynchronous I/O, consider
Parallel.ForEachAsync().
5. What is Interlocked, and when should it be used?
What is it?
Interlocked provides atomic operations on shared variables without requiring an explicit lock.
Atomic means the operation completes as one indivisible action.
Example
int counter = 0;
Parallel.For(0, 1000, i =>
{
Interlocked.Increment(ref counter);
});
Console.WriteLine(counter);
Output:
1000
Explanation:
-
Multiple threads increment the same variable.
-
Interlocked.Increment()prevents lost updates. -
The final value is always 1000 in this example.
Common Methods
|
Method |
Purpose |
|---|---|
|
|
Increase by 1 |
|
|
Decrease by 1 |
|
|
Add a specified value |
|
|
Atomically replace a value |
|
|
Replace only if the current value matches an expected value |
Key Points
-
Interlockedis useful for counters and simple shared-state updates. -
It avoids the overhead of an explicit lock.
-
It supports atomic operations on specific data types.
-
Use
lockwhen multiple operations must execute together as one critical section.
6. What is the volatile keyword?
What is it?
The volatile keyword tells the compiler and runtime that a field may be accessed by multiple threads.
It provides specific memory-ordering and visibility guarantees, but does not make compound operations atomic.
Example
class Worker
{
private volatile bool _stopRequested;
public void Run()
{
while (!_stopRequested)
{
// Perform work
}
}
public void Stop()
{
_stopRequested = true;
}
}
Explanation:
-
One thread executes
Run(). -
Another thread calls
Stop(). -
The volatile field ensures the loop checks the shared flag with the required visibility semantics.
Key Points
-
volatileis used for simple shared-state communication. -
It restricts certain compiler and processor memory reorderings.
-
It does not guarantee atomicity for
counter++. -
It does not replace
lockfor protecting multiple related operations. -
Prefer
CancellationTokenfor cancellation in modern asynchronous applications.
7. What is a Deadlock, and how can you prevent it?
What is it?
A deadlock occurs when two or more threads wait indefinitely for resources held by each other.
Diagram
Thread 1:
Acquires Lock A
Waits for Lock B
Thread 2:
Acquires Lock B
Waits for Lock A
Result:
Thread 1 waits for Thread 2
Thread 2 waits for Thread 1
DEADLOCK
Example
object lockA = new();
object lockB = new();
Task task1 = Task.Run(() =>
{
lock (lockA)
{
Thread.Sleep(100);
lock (lockB)
{
Console.WriteLine("Task 1");
}
}
});
Task task2 = Task.Run(() =>
{
lock (lockB)
{
Thread.Sleep(100);
lock (lockA)
{
Console.WriteLine("Task 2");
}
}
});
await Task.WhenAll(task1, task2);
Explanation:
-
Task 1 acquires
lockA, then attemptslockB. -
Task 2 acquires
lockB, then attemptslockA. -
If both hold their first lock simultaneously, neither can proceed.
-
The program may hang.
How to Prevent Deadlocks
-
Acquire locks in a consistent order.
-
Avoid nested locks where possible.
-
Keep critical sections short.
-
Use timeouts with
Monitor.TryEnter()when appropriate. -
Avoid blocking asynchronous code using
.Wait()or.Result.
Key Points
-
Deadlocks are caused by circular waiting.
-
Consistent lock ordering is an important prevention technique.
-
Deadlocks can occur in synchronous and asynchronous code.
-
A timeout can prevent indefinite waiting but does not automatically fix the underlying design.
8. What is the difference between Thread.Sleep() and Task.Delay()?
What is it?
-
Thread.Sleep(): Blocks the current thread for a specified duration. -
Task.Delay(): Creates an awaitable delay without blocking a thread during the wait.
Example
// Blocking
Thread.Sleep(2000);
Console.WriteLine("Sleep completed");
// Non-blocking asynchronous delay
await Task.Delay(2000);
Console.WriteLine("Delay completed");
Diagram
Thread.Sleep(2000)
------------------
Thread → Blocked for 2 seconds
→ Continue execution
await Task.Delay(2000)
----------------------
Thread → Start delay
→ Released during wait
→ Delay completes
→ Continuation executes
Comparison
|
|
|
|---|---|
|
Blocks current thread |
Does not block while awaited |
|
Synchronous |
Asynchronous |
|
Does not support cancellation tokens |
Supports |
|
Useful for specialized synchronous scenarios |
Preferred for async methods |
|
Wastes a thread during the wait |
Improves scalability during waits |
Key Points
-
Avoid
Thread.Sleep()in ASP.NET Core request handling. -
Prefer
await Task.Delay()for asynchronous delays. -
Task.Delay()does not automatically create a new thread. -
Task.Delay()is commonly used for retry intervals and polling.
9. What are Channel<T> and Producer-Consumer Patterns?
What is it?
Producer-Consumer Pattern: A design pattern where producers create work items and consumers process them independently.
Channel<T>: A thread-safe mechanism in .NET for passing data asynchronously between producers and consumers.
Diagram
Producer 1 ──┐
|
Producer 2 ──┼──> Channel<T> ──> Consumer
| Queue |
Producer 3 ──┘ |
v
Process Items
Example
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<int>();
// Producer
async Task ProducerAsync()
{
for (int i = 1; i <= 5; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
// Consumer
async Task ConsumerAsync()
{
await foreach (int item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Processing: {item}");
}
}
await Task.WhenAll(
ProducerAsync(),
ConsumerAsync()
);
Output:
Processing: 1
Processing: 2
Processing: 3
Processing: 4
Processing: 5
Explanation:
-
The producer writes numbers into the channel.
-
The consumer reads and processes the numbers.
-
ReadAllAsync()waits asynchronously when no items are available. -
Complete()signals that no more items will be written.
Real-World ASP.NET Core Example
Web API Request
|
v
Add Email Job to Channel
|
v
BackgroundService
|
v
Read Email Job
|
v
Send Email
Key Points
-
Channel<T>supports asynchronous producer-consumer communication. -
It is thread-safe for multiple producers and consumers.
-
Bounded channels can provide backpressure when consumers are slower than producers.
-
It is useful for background jobs, message processing, and work queues.
-
An in-memory channel is not durable; queued work can be lost if the application stops.
Quick Interview Revision
|
Concept |
Important Point |
|---|---|
|
|
Allows one thread at a time into a critical section |
|
|
Advanced locking with timeout and signaling |
|
|
Can synchronize across processes |
|
|
Controls concurrent access; supports async |
|
Thread Safety |
Correct behavior under concurrent access |
|
|
Parallel processing for CPU-bound operations |
|
|
Atomic operations without explicit locking |
|
|
Memory visibility and ordering, not compound atomicity |
|
Deadlock |
Circular waiting between threads or tasks |
|
|
Blocks the current thread |
|
|
Asynchronous, non-blocking delay |
|
|
Thread-safe asynchronous producer-consumer queue |
Interview focus for .NET Tech Lead: Prioritize lock, thread safety, deadlock prevention, SemaphoreSlim, Interlocked, and Task.Delay() versus Thread.Sleep(). These concepts are especially relevant when designing concurrent ASP.NET Core services.