Module 1 · Own state and coordinate concurrent work · Lesson 2 of 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.
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
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?
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?
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?
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.
TechInterviewNotes Editorial Team · Updated 27 Oct 2026 ·