C# · Course navigation

C#: protecting shared state with lock

Start two threads that update one counter and use lock so that no update is lost.

On this page
Before you start

C# threads and static fields.

  • Start two threads that update one counter and use lock so that no update is lost.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

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.

Code example

C#
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);
  }
}
Output
counter=200000

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1using System;
2using System.Threading;
3
4class Program {
5 static int counter = 0;
6 static readonly object gate = new object();
7
8 static void Work() {
9 for (int i = 0; i < 100000; i++) {
10 lock (gate) {
11 counter++;
12 }
13 }
14 }
15
16 static void Main() {
17 var a = new Thread(Work);
18 var b = new Thread(Work);
19 a.Start();
20 b.Start();
21 a.Join();
22 b.Join();
23 Console.WriteLine("counter=" + counter);
24 }
25}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

Behind the scenes

How C# processes this code

C# compiles to code for the .NET runtime, which invokes the program entry point.

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.

Further reading