Threads, locks and thread safety
Why code that works on one thread breaks on two: race conditions, lock, Interlocked, concurrent collections, deadlocks and immutable data.
Task.Run, Parallel, timers), any data they share and change is at risk. The bugs are nasty: they depend on timing, appear rarely, and often vanish in the debugger.
lock
public class ProgressTracker{ private readonly object _gate = new(); // a private object used only for locking private int _done; private readonly List<string> _log = new(); public void Complete(string mark) { lock (_gate) // one thread at a time in here { _done++; _log.Add($"{mark} finished"); } } public int Done { get { lock (_gate) return _done; } }}Lock on a private object (never this, aTypeor a string).Keep the locked section short: no I/O, no awaits, no calls to unknown code inside it. Every access (reads too) to the shared data must use the same lock. awaitis not allowed insidelock. For async code useSemaphoreSlimwithWaitAsync.
Interlocked: atomic operations on numbers
private int _count;Interlocked.Increment(ref _count);Interlocked.Add(ref _bytes, chunk.Length);int old = Interlocked.Exchange(ref _state, 2);Concurrent collections
using System.Collections.Concurrent; var cache = new ConcurrentDictionary<string, Section>();Section s = cache.GetOrAdd("IPE300", name => LoadSection(name)); // thread-safe var results = new ConcurrentBag<ClashResult>();Parallel.ForEach(pairs, p => { if (Clashes(p)) results.Add(new ClashResult(p)); }); var queue = new ConcurrentQueue<string>(); // producer/consumerDeadlocks
// Thread 1 // Thread 2lock (a) lock (b){ { lock (b) { … } // waits lock (a) { … } // waits} }// each holds what the other needs: both wait forever.Result) on a UI thread.
The best lock is no lock
Don’t share: give each thread its own data and combine results at the end. Immutable data: records with initproperties,ImmutableList<T>. Objects that never change can be read by any number of threads safely.Confine mutable state to one thread (e.g. only the UI thread touches the view model) and pass messages to it.
Deposit and Withdraw are safe when called from many threads, and Withdraw never makes the balance negative.
class Account { decimal _b; public void Deposit(decimal a) => _b += a; public bool Withdraw(decimal a) { if (_b < a) return false; _b -= a; return true; } }
Show a hint
Show the solution
public class Account{ private readonly object _gate = new(); private decimal _balance; public void Deposit(decimal amount) { lock (_gate) _balance += amount; } public bool Withdraw(decimal amount) { lock (_gate) { if (_balance < amount) return false; // check… _balance -= amount; // …and act, as one unit return true; } } public decimal Balance { get { lock (_gate) return _balance; } }}Remember
Shared mutable data + several threads = race conditions. Protect it with lock(short, private lock object) orInterlockedfor numbers.Use concurrent collections for shared collections; take locks in a fixed order. Prefer not sharing, immutable data, and confining state to one thread.