Lesson 18 / 25
Threads, Locks and Thread Safety
Protect shared state with locks, Interlocked and concurrent collections.
Shared mutable state needs protection
When several threads touch the same data, operations like count++ (read, add, write) can interleave and lose updates: a race condition. Options, from simplest: avoid sharing (give each task its own data and combine results); use immutable data; use Interlocked for single atomic operations (Interlocked.Increment(ref count)); use concurrent collections such as ConcurrentDictionary<TKey, TValue> and ConcurrentQueue<T>; or protect a block with lock. Since .NET 9, the dedicated System.Threading.Lock type is the recommended lock object. You cannot await inside a lock block; for async code use SemaphoreSlim with WaitAsync. Deadlocks happen when two threads each hold a lock the other needs; always acquire multiple locks in the same order. For producer-consumer pipelines, System.Threading.Channels offers async-friendly queues with back-pressure.
Three ways to count safely
count++ from many tasks would lose updates; each version below is correct.
using System.Collections.Concurrent;
int counter = 0;
await Task.WhenAll(Enumerable.Range(0, 100).Select(_ => Task.Run(() =>
{
for (int i = 0; i < 1000; i++) Interlocked.Increment(ref counter);
})));
var hits = new ConcurrentDictionary<string, int>();
hits.AddOrUpdate("/home", 1, (_, old) => old + 1);
var gate = new Lock();
var log = new List<string>();
void Append(string line)
{
lock (gate) { log.Add(line); } // List<T> is not thread-safe
}
var throttle = new SemaphoreSlim(3); // async-friendly: at most 3 at once
async Task CallApiAsync()
{
await throttle.WaitAsync();
try { await Task.Delay(100); }
finally { throttle.Release(); }
}Most collections are not thread-safe
List<T> and Dictionary<TKey, TValue> can be corrupted by concurrent writes, sometimes silently. Use a lock, a concurrent collection, or redesign so only one thread writes.
Quick check: Why can you not use `lock` around an `await`?
- The method may resume on a different thread, and the compiler forbids await inside lock; use SemaphoreSlim instead
- lock is deprecated
- await is not allowed in classes with locks
- lock only works with value types
Answer
The method may resume on a different thread, and the compiler forbids await inside lock; use SemaphoreSlim instead — Monitor-based locks are thread-affine, so the compiler disallows await inside lock blocks.