Quick summary: Prefer high-level concurrent types first, use lock for complex invariants, and use Interlocked for simple atomic updates; examples below show common patterns and pitfalls.
Thread safety describes the property of code or data structures that guarantees correct behavior when accessed concurrently by multiple threads. At its core it addresses two related problems: race conditions (where the result depends on the unpredictable timing of threads) and data corruption (where simultaneous writes or read/write sequences leave shared state inconsistent).
There are several complementary strategies to achieve thread safety: avoid sharing mutable state by using immutability or thread-local data; use synchronization primitives (locks, semaphores, interlocked operations) to serialize access to critical sections; and use concurrent collections and lock-free primitives that encapsulate safe coordination for common patterns. The .NET runtime provides a set of concurrent collection types designed to let multiple threads add and remove items safely without custom locking, which simplifies many common scenarios.
Thread safety is not a single switch you turn on; it is a design discipline. You must decide which invariants must hold across operations and then choose the minimal synchronization that preserves those invariants while keeping contention low. For many applications, adopting the Task-based model and async patterns reduces the need for low-level thread management, but concurrency still requires careful thought about shared resources and lifecycle of objects.
Use the classes in System.Collections.Concurrent for shared collections that multiple threads will read and write concurrently; they provide built‑in synchronization and scalability without custom locks.
The lock statement (Monitor) is the simplest way to protect a critical section. Use it for short, well‑defined operations; avoid holding locks across I/O or long computations to reduce contention and deadlock risk.
For simple atomic updates (counters, flags), prefer Interlocked methods or volatile fields rather than full locks. These are lighter weight but only suitable for simple patterns (no compound invariants).
Favor the Task-based model and async/await for concurrency rather than manually creating threads; tasks simplify error handling and resource management and integrate with the thread pool.
// Thread-safe counter using lock
public class LockedCounter {
private int _count;
private readonly object _sync = new object();
public void Increment() {
lock (_sync) {
_count++;
}
}
public int Get() {
lock (_sync) {
return _count;
}
}
}
// Atomic counter using Interlocked
using System.Threading;
public class AtomicCounter {
private int _count;
public void Increment() {
Interlocked.Increment(ref _count);
}
public int Get() {
return Volatile.Read(ref _count);
}
}
// Using ConcurrentDictionary
using System.Collections.Concurrent;
var dict = new ConcurrentDictionary<string,int>();
dict.AddOrUpdate("key", 1, (k, old) => old + 1);
if (dict.TryGetValue("key", out var v)) {
Console.WriteLine(v);
}
// Async lock pattern with SemaphoreSlim
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1,1);
public async Task<T> WithLockAsync<T>(Func<Task<T>> work) {
await _sem.WaitAsync();
try {
return await work();
} finally {
_sem.Release();
}
}