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.