C#: protecting shared state with lock
Start two threads that update one counter and use lock so that no update is lost.
using System;
using System.Threading;
class Program {
static int counter = 0;
static readonly object gate = new object();
static void Work() {
for (int i = 0; i < 100000; i++) {
lock (gate) {
counter++;
}
}
}
static void Main() {
var a = new Thread(Work);
var b = new Thread(Work);
a.Start();
b.Start();
a.Join();
b.Join();
Console.WriteLine("counter=" + counter);
}
}counter=200000
Explanation
When two threads change the same variable without coordination, the result is unpredictable. counter++ is a read, an add, and a write, so interleaved threads can overwrite each other's updates. The part of the code that touches shared state is called the critical section.
The lock statement takes a monitor on an object, runs the block, and releases the monitor even if an exception occurs. Only one thread can hold it at a time. Join blocks until a thread finishes. Always lock on a private object that is not visible to outside code.
Explore the dry run
using System;using System.Threading; class Program { static int counter = 0; static readonly object gate = new object(); static void Work() { for (int i = 0; i < 100000; i++) { lock (gate) { counter++; } } } static void Main() { var a = new Thread(Work); var b = new Thread(Work); a.Start(); b.Start(); a.Join(); b.Join(); Console.WriteLine("counter=" + counter); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A private object serves as the lock. | — | — | |
| Only one thread at a time may run the block; the other waits. | — | — | |
| Wait for thread a, then for b, before reading counter. | — | — | |
| Both threads completed 100000 protected increments. | counter = 200000 | counter=200000 |
Cost and common mistakes
O(n) total work; the lock serializes the increments.
- Never lock on this, a type, or a string; other code could lock on the same object and cause deadlocks.
- Interlocked.Increment(ref counter) is a cheaper way to protect a single integer.
- Holding a lock while doing slow work blocks all other threads that need it.
Try it yourself
Which class makes a single increment atomic without a lock?
Reveal answer and reasoning
System.Threading.Interlocked: Interlocked.Increment(ref counter) performs the update atomically.