Advanced 13 min

Mutex and Lock

Learn to synchronize thread access to shared data using mutexes and locks to prevent data races

Learn how to protect shared data from race conditions using mutexes and lock guards in multithreaded programs.

A Simple Example

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex counterMutex;
int sharedCounter{0};

void incrementCounter(int iterations) {
    for (int i{0}; i < iterations; ++i) {
        std::lock_guard<std::mutex> lock{counterMutex};
        ++sharedCounter;
    }  // lock automatically released here
}

int main() {
    const int iterations{1000};
    std::vector<std::thread> threads;

    // Launch 5 threads that all increment the counter
    for (int i{0}; i < 5; ++i) {
        threads.emplace_back(incrementCounter, iterations);
    }

    // Wait for all threads to finish
    for (auto& thread : threads) {
        thread.join();
    }

    std::cout << "Final counter: " << sharedCounter << "\n";
    std::cout << "Expected: " << (5 * iterations) << "\n";

    return 0;
}

Breaking It Down

std::mutex - Mutual Exclusion

  • What it does: Ensures only one thread can access a critical section at a time
  • lock(): Blocks until mutex is available, then locks it
  • unlock(): Releases the mutex for other threads
  • Remember: Always pair lock() with unlock() or use lock guards

std::lock_guard - RAII Locking

  • What it does: Automatically locks mutex in constructor, unlocks in destructor
  • Benefit: Exception-safe - mutex unlocks even if exception occurs
  • Usage: std::lock_guard<std::mutex> lock{myMutex};
  • Remember: Prefer lock_guard over manual lock/unlock

Race conditions

  • What it is: Multiple threads accessing shared data, at least one writing
  • Problem: Operations like ++ are not atomic - read, modify, write can interleave
  • Solution: Protect all access to shared data with the same mutex
  • Remember: Even simple operations need protection in multithreaded code

Critical sections

  • What it is: Code that accesses shared data and must not run concurrently
  • Keep it small: Long critical sections reduce parallelism
  • Lock scope: Use braces {} to limit lock_guard lifetime
  • Remember: Only protect what needs protection, no more

Why This Matters

  • When multiple threads access shared data simultaneously, race conditions cause unpredictable behavior and bugs that are hard to reproduce.
  • Mutexes (mutual exclusion) ensure only one thread accesses critical sections at a time, preventing data corruption.
  • Lock guards provide RAII-style automatic locking and unlocking, making thread-safe code easier to write correctly.

Critical Insight

A mutex is like a bathroom key. Only the person holding the key can enter the bathroom (critical section). When they are done, they return the key so someone else can use it.

lock_guard is like a smart key holder that automatically returns the key when you leave the bathroom, even if you trip and fall. Without it, you might forget to return the key (forget to unlock), and everyone else is stuck waiting forever.

Best Practices

Always use lock_guard: Prefer std::lock_guard over manual lock/unlock for exception safety and simplicity.

Keep critical sections small: Lock only what you need and for as short as possible to maximize parallelism.

One mutex per shared resource: Each piece of shared data should have its own mutex to avoid unnecessary contention.

Consistent locking order: If you need multiple mutexes, always lock them in the same order to avoid deadlocks.

Use braces to limit lock scope: Create a new scope {} to limit lock_guard lifetime to just the critical section.

Common Mistakes

Forgetting to lock: Even simple operations like ++ need mutex protection in multithreaded code.

Locking too much: Holding locks too long reduces parallelism. Keep critical sections minimal.

Deadlocks: Locking multiple mutexes in different orders can cause threads to wait for each other forever.

Returning with lock held: Returning from a function with a manually locked mutex causes deadlock. Use lock_guard.

Debug Challenge

This code has a race condition. Click the highlighted line to fix it:

1 std::mutex dataMutex;
2 int sharedData{0};
3
4 void updateData() {
5 ++sharedData;
6 }

Quick Quiz

  1. Why use std::lock_guard instead of manual lock/unlock?
Automatic unlock even if exception occurs (RAII)
It is faster than manual locking
It uses less memory
  1. What is a race condition?
When one thread runs faster than another
Multiple threads accessing shared data with at least one writing
When threads finish in the wrong order
  1. When does lock_guard release the mutex?
When you call unlock() on it
When the thread finishes
When it goes out of scope (destructor runs)

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once