Sharing data safely in a multi threaded C#
- Part 1: Synchronization Primitives .Net/C#
- Part 2: Sharing data safely in a multi threaded C#
1. Thread-Safe Data Structures
Concurrent Collections: Use thread-safe collections from System.Collections.Concurrent
for scenarios where you need to share data across multiple threads.
ConcurrentDictionary<TKey, TValue>
ConcurrentQueue<T>
ConcurrentStack<T>
BlockingCollection<T>
(which wraps around other collections and adds thread-safe operations)
using System.Collections.Concurrent;
var concurrentQueue = new ConcurrentQueue<int>();
// Producer thread
Task.Run(() =>
{
concurrentQueue.Enqueue(1);
});
// Consumer thread
Task.Run(() =>
{
if (concurrentQueue.TryDequeue(out int result))
{
Console.WriteLine(result);
}
});
2. Locks
lock
Statement: The lock
statement (or Monitor
) is used to ensure that only one thread can access a critical section of code at a time.
private static readonly object _lock = new object();
private static int sharedValue;
// Thread 1
Task.Run(() =>
{
lock (_lock)
{
sharedValue++;
}
});
// Thread 2
Task.Run(() =>
{
lock (_lock)
{
Console.WriteLine(sharedValue);
}
});
Monitor
Class: Provides more advanced control over synchronization compared to lock
.
private static readonly object _lock = new object();
private static int sharedValue;
// Thread 1
Task.Run(() =>
{
Monitor.Enter(_lock);
try
{
sharedValue++;
}
finally
{
Monitor.Exit(_lock);
}
});
// Thread 2
Task.Run(() =>
{
Monitor.Enter(_lock);
try
{
Console.WriteLine(sharedValue);
}
finally
{
Monitor.Exit(_lock);
}
});
3. ReaderWriterLockSlim
ReaderWriterLockSlim
: Provides a way to handle scenarios where multiple threads read data simultaneously but write data exclusively.
using System.Threading;
private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private static int sharedValue;
// Thread for writing
Task.Run(() =>
{
_lock.EnterWriteLock();
try
{
sharedValue++;
}
finally
{
_lock.ExitWriteLock();
}
});
// Thread for reading
Task.Run(() =>
{
_lock.EnterReadLock();
try
{
Console.WriteLine(sharedValue);
}
finally
{
_lock.ExitReadLock();
}
});
4. Interlocked
Class
Interlocked
Class: Provides atomic operations for variables shared by multiple threads, useful for simple data types like integers.
using System.Threading;
private static int sharedValue;
// Thread 1
Task.Run(() =>
{
Interlocked.Increment(ref sharedValue);
});
// Thread 2
Task.Run(() =>
{
Console.WriteLine(Interlocked.CompareExchange(ref sharedValue, 0, 0));
});
5. Semaphore
and SemaphoreSlim
Semaphore
: Useful for controlling access to a resource pool with a limited number of slots.
using System.Threading;
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private static int sharedValue;
// Thread 1
Task.Run(async () =>
{
await _semaphore.WaitAsync();
try
{
sharedValue++;
}
finally
{
_semaphore.Release();
}
});
// Thread 2
Task.Run(async () =>
{
await _semaphore.WaitAsync();
try
{
Console.WriteLine(sharedValue);
}
finally
{
_semaphore.Release();
}
});
Best Practices
- Minimize Lock Contention: Try to minimize the time spent within locked sections to avoid performance bottlenecks.
- Avoid Deadlocks: Ensure that locks are always acquired and released in a consistent order to prevent deadlocks.
- Prefer High-Level Concurrency Utilities: Use higher-level abstractions like
Concurrent
collections orTask
parallelism where possible to avoid low-level synchronization issues.
By using these techniques and following best practices, you can safely share and manage data across multiple threads in C#.
Thank you for reading! If you have any questions or suggestions, feel free to connect with me on LinkedIn.