<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Advanced C#
All lessons in this course

Lesson 50 of 60 15 minAdvanced

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.

Once several threads run at the same time (async continuations, 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.

Interactive

Two threads, one counter

counter++ looks like one step, but the CPU does three: read the value, add 1, write it back. Threads A and B each do it once. The answer should be 2.

Thread Alocal: –
shared memorycounter = 0
Thread Blocal: –
  1. start: counter = 0
Both threads are about to run counter++. Press Step.

lock

Lock.cs
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, a Type or 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.
  • await is not allowed inside lock. For async code use SemaphoreSlim with WaitAsync.

Interlocked: atomic operations on numbers

Interlocked.cs
private int _count;Interlocked.Increment(ref _count);Interlocked.Add(ref _bytes, chunk.Length);int old = Interlocked.Exchange(ref _state, 2);

Concurrent collections

Concurrent.cs
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/consumer

Deadlocks

Deadlock.cs
// Thread 1                       // Thread 2lock (a)                          lock (b){                                 {    lock (b) { … }   // waits         lock (a) { … }   // waits}                                 }// each holds what the other needs: both wait forever

Avoid deadlocks by always taking multiple locks in the same order, by holding locks briefly, and by never blocking on async code (.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 init properties, 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.
Quick check

Why can counter++ lose updates when two threads run it at once?

Quick check

Which is the right way to protect a shared Dictionary that many threads add to?

Practice

Fix this class so 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

The check and the subtraction in Withdraw must happen inside the SAME lock, or another thread can withdraw in between.

Show the solution
Solution.cs
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) or Interlocked for numbers.
  • Use concurrent collections for shared collections; take locks in a fixed order.
  • Prefer not sharing, immutable data, and confining state to one thread.
0 of 60 lessons completed