C++ · Course navigation

C++: protecting shared data with std::mutex

Run two threads that increment one counter and use a lock to keep every update.

On this page
Before you start

C++ lambdas and std::thread.

  • Run two threads that increment one counter and use a lock to keep every update.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A data race occurs when two threads access the same memory at the same time and at least one writes, with no synchronization. In C++ a data race is undefined behaviour, not just a wrong answer. The statement ++counter is a read, an add, and a write, so concurrent increments can be lost.

A std::mutex provides mutual exclusion. std::lock_guard locks it when constructed and unlocks it when it goes out of scope, even if an exception is thrown, which is the RAII pattern. join waits for a thread to finish. With the lock in place both threads together always produce 200000.

Code example

C++
#include <iostream>
#include <mutex>
#include <thread>

int main() {
  int counter = 0;
  std::mutex m;

  auto work = [&]() {
    for (int i = 0; i < 100000; ++i) {
      std::lock_guard<std::mutex> lock(m);
      ++counter;
    }
  };

  std::thread a(work);
  std::thread b(work);
  a.join();
  b.join();
  std::cout << "counter=" << counter << "\n";
}
Output
counter=200000

Follow the code

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

Show guided walkthrough
Guided walkthroughReady
1#include <iostream>
2#include <mutex>
3#include <thread>
4
5int main() {
6 int counter = 0;
7 std::mutex m;
8
9 auto work = [&]() {
10 for (int i = 0; i < 100000; ++i) {
11 std::lock_guard<std::mutex> lock(m);
12 ++counter;
13 }
14 };
15
16 std::thread a(work);
17 std::thread b(work);
18 a.join();
19 b.join();
20 std::cout << "counter=" << counter << "\n";
21}

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
One mutex guards the counter. Every thread must use the same one.—
—
Lock now; the lock is released automatically at the end of the loop body.only one thread holds the lock
—
Wait for thread a to finish before reading counter.only one thread holds the lock
—
Both threads did 100000 protected increments.counter = 200000
counter=200000

Behind the scenes

How C++ processes this code

The C++ compiler produces a native executable. Execution enters main, where objects and function calls perform the work.

Cost and common mistakes

O(n) work; the lock serializes the updates.

  • Destroying a joinable std::thread calls std::terminate. Always join or detach.
  • Locking inside the innermost loop is slow. Count locally and add the total under the lock once, or use std::atomic.
  • On older Linux toolchains the program must be linked with -pthread.

Try it yourself

How could you avoid the mutex for a plain counter?

Reveal answer and reasoning

Use std::atomic<int> and call fetch_add(1), which makes each increment atomic without a lock.

Further reading