Question

Volatile, Interlocked, and lock in C#: Thread-Safe Counters

csharpmultithreadinglockingvolatileinterlocked

Question

A class has a public int counter field accessed by multiple threads. The field is only incremented and decremented. Which approach should be used to increment it safely, and why?

lock (this.locker)
{
    this.counter++;
}

Interlocked.Increment(ref this.counter);

public volatile int counter;

After discovering volatile, is it safe to replace many lock statements and uses of Interlocked with volatile? Why or why not?

Short Answer

You will learn the difference between visibility, atomic operations, and mutual exclusion in C#. By the end, you will know why Interlocked.Increment is usually the right tool for a simple shared counter, why volatile does not make counter++ safe, and when a lock is necessary.

Concept

counter++ looks like one operation, but it is a read-modify-write sequence:

counter = counter + 1;

A thread must read the current value, calculate a new value, and write that value back. If two threads do this at the same time, one update can overwrite the other. This is a race condition.

For a counter that is only incremented or decremented, use Interlocked:

Interlocked.Increment(ref counter);
Interlocked.Decrement(ref counter);

Interlocked performs the whole read-modify-write operation atomically. No other thread can observe or perform a conflicting update in the middle of it.

volatile solves a different problem: memory visibility and ordering. It tells the runtime and CPU that reads and writes of that field must be observed appropriately across threads. It does not combine multiple operations into one atomic operation. Therefore, this remains unsafe even when counter is volatile:

volatileCounter++; // Still read, add, then write: not atomic

A lock provides mutual exclusion. Only one thread can execute code guarded by the same lock object at a time. It is needed when an update involves multiple values, conditions, or operations that must stay consistent together.

Avoid a public mutable counter field. Prefer a private field and expose a method or read-only property so callers cannot update it unsafely.

Mental Model

Imagine a shared whiteboard showing a score.

  • volatile is like making sure every person looks at the latest visible writing before acting. It does not stop two people from erasing and rewriting the score simultaneously.
  • Interlocked.Increment is like a score machine that adds exactly one point as a single indivisible action.
  • lock is like giving one person the key to the whiteboard cabinet. While they hold the key, nobody else can read or change the protected work inside.

For adding or subtracting one number, use the score machine: Interlocked. For a larger operation involving several related pieces of state, use the key: lock.

Take Quiz

Syntax and Examples

For a simple numeric counter, declare the field as private:

using System.Threading;

public sealed class RequestCounter
{
    private int _count;

    public void AddRequest()
    {
        Interlocked.Increment(ref _count);
    }

    public void RemoveRequest()
    {
        Interlocked.Decrement(ref _count);
    }

    public int CurrentCount => Volatile.Read(ref _count);
}

Interlocked.Increment and Interlocked.Decrement safely update _count even when many threads call the methods concurrently.

For a more complex update, use a private lock object:

public sealed class Inventory
{
    private readonly object _sync = new object();
    private int _available;
    private int _reserved;

    public bool TryReserveOne()
    {
        lock (_sync)
        {
            if (_available == 0)
            {
                return false;
            }

            _available--;
            _reserved++;
            return true;
        }
    }
}

Both fields must change together. Two separate interlocked operations would not make the whole reservation transaction atomic.

volatile can be appropriate for a simple signal, such as a flag that one thread writes and another thread reads:

private volatile bool _stopRequested;

public void RequestStop()
{
    _stopRequested = true;
}

public void Run()
{
    while (!_stopRequested)
    {
        // Perform work.
    }
}

In modern C#, Volatile.Read and Volatile.Write are often preferred when you want the visibility semantics at a particular access point.

Step by Step Execution

Consider this unsafe code when _count starts at 0:

_count++;

Two threads can interleave as follows:

  1. Thread A reads _count and gets 0.
  2. Thread B reads _count and gets 0.
  3. Thread A calculates 0 + 1 and writes 1.
  4. Thread B calculates 0 + 1 and writes 1.
  5. The final value is 1, even though two increments were requested.

With Interlocked.Increment:

Interlocked.Increment(ref _count);
  1. Thread A atomically changes _count from 0 to 1.
  2. Thread B atomically changes _count from 1 to 2.
  3. The final value is correctly 2.

volatile does not change the unsafe trace. It may ensure that each thread sees recent writes, but both threads can still read the same old value before either writes its result.

Real World Use Cases

Common uses for Interlocked counters include:

  • Counting processed messages in a background worker.
  • Tracking requests handled by a web service.
  • Counting cache hits and misses for diagnostics.
  • Tracking active operations with increment-on-start and decrement-in-finally.
  • Generating simple numeric sequence values with Interlocked.Increment.

Common uses for lock include:

  • Checking a balance and subtracting from it as one operation.
  • Updating a collection and related metadata together.
  • Protecting a queue, dictionary, or custom object with multiple invariants.
  • Ensuring only one thread runs an initialization or state transition.

Common uses for volatile include:

  • A simple shutdown or cancellation-style flag when a full synchronization primitive is not required.
  • Publishing or reading a single value where visibility is the main need.

For cancellation in application code, CancellationToken is usually a clearer and more complete option than implementing a custom volatile flag.

Take Quiz

Real Codebase Usage

A common production pattern is to keep the field private and provide safe operations:

private int _activeJobs;

public void StartJob()
{
    Interlocked.Increment(ref _activeJobs);
}

public void FinishJob()
{
    Interlocked.Decrement(ref _activeJobs);
}

public int ActiveJobs => Volatile.Read(ref _activeJobs);

When decrementing after work that may throw, place it in finally:

Interlocked.Increment(ref _activeJobs);
try
{
    ProcessJob();
}
finally
{
    Interlocked.Decrement(ref _activeJobs);
}

For multi-step state changes, use a lock with a dedicated private object:

private readonly object _sync = new object();

public bool TryMoveItem(Item item)
{
    lock (_sync)
    {
        if (!_source.Contains(item))
        {
            return false; // Guard clause while state is protected.
        }

        _source.Remove(item);
        _destination.Add(item);
        return true;
    }
}

Do not use lock (this). External code can also lock your public instance, potentially causing unexpected blocking or deadlocks. A private readonly lock object prevents outside code from participating in the lock.

Common Mistakes

Assuming volatile makes ++ safe

This is incorrect:

private volatile int _count;

public void Add()
{
    _count++; // Not thread-safe
}

Use Interlocked.Increment(ref _count) instead.

Locking on this

Avoid this:

lock (this)
{
    _count++;
}

Any caller with a reference to the object can write lock (instance), interfering with your synchronization. Use a private object:

private readonly object _sync = new object();

Reading a counter and then acting on it

Even if individual reads and writes are atomic for int, this check-and-update is not atomic:

if (_count > 0)
{
    _count--;
}

Another thread can change _count after the check. Use a lock or an Interlocked.CompareExchange loop when the condition and update must occur together.

Using separate interlocked calls for one transaction

This does not make both updates one atomic unit:

Interlocked.Decrement(ref _available);
Interlocked.Increment(ref _reserved);

If the values must always agree, protect the full operation with a lock.

Exposing mutable shared fields publicly

A public field lets any caller bypass your safe methods. Keep shared state private and expose operations such as Increment, TryReserve, or a read-only property.

Replacing every lock with volatile

A lock protects a block of code and establishes synchronization around it. volatile protects neither a block nor a compound operation. Replace a lock only after confirming that the operation truly needs only atomic access or visibility.

Comparisons

ToolMain purposeIs counter++ safe?Best use
volatile / Volatile.ReadVisibility and memory ordering for individual reads and writesNoSimple cross-thread flags or single-value publication
InterlockedAtomic operations on a single supported valueYes, with Interlocked.IncrementCounters, atomic exchange, compare-and-swap
lockMutual exclusion for a code blockYes, inside the lockMulti-step operations and related shared state

Interlocked versus lock

  • Prefer Interlocked for a direct operation such as increment, decrement, add, exchange, or compare-and-swap on one variable.
  • Prefer lock when correctness depends on several statements executing together.
  • Interlocked is typically lighter for simple counters, but correctness is more important than choosing the smallest primitive.

volatile versus Interlocked

  • volatile does not prevent conflicting updates.
  • Interlocked provides atomic updates and the required memory-ordering behavior for its operations.
  • An int read/write is generally atomic in C#, but atomic reads and writes alone do not make a multi-step calculation atomic.
Take Quiz

Cheat Sheet

// Thread-safe add/subtract for one integer.
Interlocked.Increment(ref _count);
Interlocked.Decrement(ref _count);
Interlocked.Add(ref _count, amount);

// Safely read a value that other threads update with Interlocked.
int current = Volatile.Read(ref _count);

// Protect a multi-step operation.
private readonly object _sync = new object();

lock (_sync)
{
    // Read, validate, and update related shared state.
}
  • x++ is a read-modify-write operation, not a safe shared-counter update.
  • volatile does not make x++, x += y, or check-then-act code atomic.
  • Use Interlocked for simple operations on one variable.
  • Use lock for multiple statements or multiple related variables.
  • Use a private readonly lock object, not this, a string, or a publicly accessible object.
  • Keep shared mutable fields private.

FAQ

Does volatile int make incrementing thread-safe in C#?

No. counter++ still performs a separate read, calculation, and write. Use Interlocked.Increment or a lock.

Should I use Interlocked.Increment or lock for a counter?

Use Interlocked.Increment when the operation is only incrementing or decrementing one numeric field. Use lock when the counter update is part of a larger operation that must remain consistent.

Is reading an int thread-safe in C#?

A normal int read or write is atomic, but atomicity alone does not address visibility, ordering, or compound logic. For a value concurrently updated with Interlocked, Volatile.Read is an explicit safe reading pattern.

Why is lock(this) discouraged?

Other code can lock the same object because it has access to this. That makes your synchronization vulnerable to accidental contention and deadlocks. Lock a private object instead.

Can I use Interlocked with long values?

Yes. Interlocked supports several numeric types, including int and long, along with operations such as exchange and compare-and-swap.

When is volatile useful?

It is useful when one thread writes a simple value or flag and other threads need to observe that write with appropriate memory visibility. It is not a replacement for mutual exclusion or atomic compound updates.

Does a lock also handle memory visibility?

Yes. Entering and leaving the same lock provides synchronization and memory-ordering guarantees in addition to ensuring that only one thread executes the protected block at a time.

Take Quiz

Mini Project

Description

Build a thread-safe job tracker for a background processing service. Multiple worker tasks will start and finish jobs concurrently, while the application reports how many jobs are currently active. This demonstrates when Interlocked is sufficient for a single counter.

Goal

Create a tracker that reports an accurate active-job count after concurrent jobs complete.

Requirements

Use a private int field to store the active-job count.

Increment the count when a job starts and decrement it when the job finishes.

Ensure the decrement happens even if job processing fails.

Run several jobs concurrently and print the final active-job count.

Do not use volatile as a replacement for the atomic updates.

Take Quiz

Keep learning

Related questions

View more