Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close
    Lesson2 / 20

    Controlled concurrency: lock versus Interlocked

    The ledger starts at (1000, 0). Four workers each move one unit 100 times. Would making each balance update atomic also make the whole transfer safe?

    Run the paired update

    Predict the final pair, the completion count and whether a later request for 601 can succeed. The pair must remain nonnegative and sum to 1000. The independent completed counter counts successful iterations.

    C#
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    var ledger = new Ledger();
    int completed = 0;
    Task[] workers = Enumerable.Range(0, 4).Select(_ => Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            if (!ledger.TryMove(1))
                throw new InvalidOperationException("Unexpected rejection.");
            Interlocked.Increment(ref completed);
        }
    })).ToArray();
    await Task.WhenAll(workers);
    var snapshot = ledger.Snapshot();
    bool rejected = !ledger.TryMove(601);
    if (snapshot.Left + snapshot.Right != 1000 || completed != 400)
        throw new InvalidOperationException("Invariant broken.");
    Console.WriteLine($"Left={snapshot.Left}; Right={snapshot.Right}");
    Console.WriteLine($"Completed={completed}; Rejected={rejected}");
    
    sealed class Ledger
    {
        private readonly Lock _gate = new();
        private int _left = 1000;
        private int _right;
    
        public bool TryMove(int amount)
        {
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
            lock (_gate)
            {
                if (amount > _left) return false;
                int nextRight = checked(_right + amount);
                _left -= amount;
                _right = nextRight;
                return true;
            }
        }
    
        public (int Left, int Right) Snapshot()
        {
            lock (_gate) return (_left, _right);
        }
    }

    Find the shared boundary

    Text
    operation                    synchronization boundary
    check left + update pair     one lock on private _gate
    read a consistent pair       the same private _gate
    increment completed          one Interlocked.Increment
    join and final output        coordinator after WhenAll

    Each accepted move preserves the sum: the first changes (1000, 0) to (999, 1). After 400 moves the pair is (600, 400), and 601 is rejected without mutation. Worker order does not change these results.

    Read TryMove beside Snapshot. Both lock the same private _gate: the writer checks availability and changes both fields within one critical section; the reader takes the pair within that same section. Two individually valid integer reads could otherwise observe different transfers. The completed counter is telemetry, not a third field in that paired snapshot.

    Two Interlocked updates would still leave a gap between them and would not include the source check. One Interlocked.Increment is sufficient for the unrelated counter. Select the boundary from the rule being protected.

    Why are two Interlocked.Add calls insufficient for this ledger contract?

    A Interlocked cannot update integers.
    B They always run on the same thread.
    C The pair and source check would still span separate operations.
    D Interlocked silently changes the initial total.
    Show answer

    Correct answer: C

    Separate atomic field updates leave observable intermediate states and do not include the balance check.

    Option A: Interlocked supports integer operations.

    Option B: Thread identity does not remove the gap between operations.

    Option D: Interlocked does not invent values; the missing transaction boundary is the issue.

    Repair a misplaced check

    On paper, move the insufficient-balance check outside the lock. Construct a schedule in which two callers both approve spending the same remaining balance. Identify the smallest region that must move back inside.

    Stop before reading the repair.

    Hint: neither caller's view of the balance stays current while the other is allowed to update it.

    Repair and verification

    Keep the availability check and both dependent assignments under the same _gate. Every concurrent reader of the pair must use that gate too. Protecting only assignments leaves the decision based on stale state.

    As a numeric check, use five workers with 120 iterations. Expect (400, 600) and 600 completions; change the rejection probe to 401 and the assertion to 600. These checks support the rule, while the bad schedule explains why the boundary matters.

    The checked addition is computed before either assignment, so a failure cannot leave a half-applied pair. lock releases the gate when its body exits, including by exception. Keep the body short: this one performs validation and arithmetic, without awaited work or unknown callbacks.

    Which reader preserves the ledger's paired snapshot contract?

    A One that locks the same private gate while reading both fields.
    B One that reads Left, yields, then reads Right.
    C One that locks a fresh object on each call.
    D One that assumes int reads make a pair atomic.
    Show answer

    Correct answer: A

    Readers and writers must cooperate on the same gate to see a consistent pair.

    Option B: A move can occur between the reads.

    Option C: Different lock objects do not exclude each other.

    Option D: Atomic individual reads do not provide a multi-field snapshot.

    What happens to the gate if checked arithmetic throws inside lock?

    A The gate is held until garbage collection.
    B The lock statement releases it while unwinding.
    C Every other task is canceled automatically.
    D The completed counter is rolled back automatically.
    Show answer

    Correct answer: B

    The lock statement guarantees release on exit, including exceptional exit.

    Option A: Lock release is scoped; it does not await collection.

    Option C: Lock has no task-cancellation policy.

    Option D: Synchronization does not roll back unrelated state.

    Review question: why would moving the counter increment into the lock still fail to make an external, unlocked pair-plus-counter read consistent?

    References: C# lock statement, Interlocked.

    Quick reference

    • Use one private gate for every concurrent access to a protected invariant.
    • Keep check and dependent updates in the same critical section.
    • Interlocked handles supported atomic operations on individual variables.
    • Atomic operations do not compose into an automatic multi-field transaction.
    • Compute fallible values before committing state; keep lock bodies short.
    Sign in to mark lessons done and keep your place in the course.Sign in

    TechInterviewNotes Editorial Team · Updated 27 Oct 2026 ·