Skip to content

Lock Statement ​

When multiple threads can access the same mutable data, the program may behave incorrectly unless access is coordinated. The lock statement is one of the simplest built-in ways to protect a critical section.

This topic matters because concurrent code introduces a new class of bugs. The code may compile and look correct, but different thread interleavings can still produce wrong results.

The problem: race conditions ​

A race condition happens when multiple threads read and write shared state without proper synchronization.

For example, imagine two threads doing this at the same time:

  1. read counter
  2. add 1
  3. write the updated value back

If both threads read the same old value before either writes the new one, one update can be lost.

What lock does ​

lock allows only one thread at a time to execute the protected block for a given lock object.

mermaid
flowchart TD
    T1["Thread A"] --> L{"Acquire lock?"}
    T2["Thread B"] --> L
    L -- Yes --> C["Enter critical section"]
    L -- No --> W["Wait until lock is released"]
    C --> R["Release lock"]
    R --> L

This is the basic idea: one thread enters, others wait.

Basic syntax ​

csharp
object gate = new();
int counter = 0;

lock (gate)
{
    counter++;
}

The object inside lock (...) is the synchronization token. All threads that need to coordinate must lock on the same object.

Why the block matters ​

Everything inside the lock block is treated as one protected region.

That means you should keep the block:

  • small
  • focused
  • limited to the shared state that needs protection

Large lock blocks increase waiting and make concurrency slower.

A worked example ​

Suppose a banking application updates a shared balance from multiple threads.

csharp
object balanceLock = new();
decimal balance = 1000m;

void Withdraw(decimal amount)
{
    lock (balanceLock)
    {
        if (amount <= balance)
        {
            balance -= amount;
            Console.WriteLine($"Withdrew {amount:C}. New balance: {balance:C}");
        }
        else
        {
            Console.WriteLine("Insufficient funds.");
        }
    }
}

The whole read-check-update sequence is inside the lock. That is important.

If only part of that logic were protected, another thread could change balance between the check and the update.

A useful mental model ​

Think of lock as “reserve exclusive access to this shared thing while I do a small, important operation.”

It is not a general tool for making complex code safe by default. It protects a specific critical section.

Lock-object guidelines ​

Prefer locking on a private dedicated object:

csharp
private readonly object _gate = new();

Avoid locking on:

  • this
  • strings
  • publicly accessible objects
  • type objects unless you intentionally want application-wide coordination

Those choices can create unexpected interactions because other code may also lock on the same object.

Common mistakes ​

  • Locking on the wrong object so threads are not actually coordinated.
  • Using different lock objects for the same shared state.
  • Holding the lock while doing slow work that does not need protection.
  • Assuming lock makes all concurrency problems disappear automatically.

Summary ​

The lock statement protects a critical section so only one thread at a time can execute it for a given synchronization object.

The key ideas are:

  • use it for shared mutable state
  • protect the whole read-modify-write sequence
  • keep the locked region small
  • use a dedicated private lock object

Concurrency is a deeper topic than one keyword, but lock gives you a practical first tool for making shared-state access safer.

Practice ​

Write a sample with a shared int counter and a dedicated lock object, then increment the counter inside a lock block.

As a second exercise, explain why locking on this is usually a weaker design than locking on a private field.

Built with VitePress and published on GitHub Pages.