Synchronization

Back

Loading concept...

C++ Concurrency: Synchronization Magic

The Toy Factory Story

Imagine you have a toy factory with many workers (threads). They all share the same toy box (shared data). Without rules, chaos happens! One worker might grab a toy another worker is painting. Disaster!

Synchronization is like having a traffic light at the toy box. It tells workers when they can touch shared toys and when they must wait.


What is a Race Condition?

The Cookie Jar Problem

Two kids want cookies from the same jar at the same time.

graph TD A["Jar has 10 cookies"] --> B["Kid 1 reads: 10"] A --> C["Kid 2 reads: 10"] B --> D["Kid 1 takes 1, writes 9"] C --> E["Kid 2 takes 1, writes 9"] D --> F["Jar shows 9"] E --> F F --> G["Expected: 8, Got: 9!"]

Race Condition = When two threads read and write shared data at the same time, and the result depends on who finishes first.

Simple Example

int cookies = 10;

void takeCookie() {
    int current = cookies;
    cookies = current - 1;
}
// Thread 1 and Thread 2
// both call takeCookie()
// Result: Unpredictable!

The Problem: Both threads read 10, both write 9. We lost a cookie count!


Mutexes: The Bathroom Lock

What is a Mutex?

A mutex is like a bathroom lock. Only one person can use the bathroom at a time.

  • Lock = Enter bathroom, close door
  • Unlock = Leave bathroom, open door
  • Others must wait outside

How Mutex Works

#include <mutex>

std::mutex toyBoxLock;
int toys = 10;

void takeToy() {
    toyBoxLock.lock();   // Enter bathroom
    toys = toys - 1;     // Use bathroom
    toyBoxLock.unlock(); // Leave bathroom
}

Rule: Always unlock! If you forget, everyone waits forever.

The Danger of Forgetting

void badExample() {
    mutex.lock();
    // What if crash here?
    // Program throws exception?
    // mutex stays locked forever!
    mutex.unlock();
}

This is why we need Lock Guards…


Lock Guards: The Automatic Door

The Problem Lock Guards Solve

Imagine a door that automatically closes when you leave. You never forget to close it!

Lock Guard = A smart helper that:

  • Locks the mutex when created
  • Unlocks automatically when done

Lock Guard Example

#include <mutex>

std::mutex m;

void safeFunction() {
    std::lock_guard<std::mutex> guard(m);
    // Do stuff safely
    // guard unlocks automatically!
}
// Even if exception happens,
// the door closes safely

Why it’s amazing: Even if your code crashes, the lock guard says “Don’t worry, I’ll unlock before I go!”

Visual Comparison

graph TD subgraph Without Lock Guard A1["lock"] --> B1["work"] B1 --> C1["crash?"] C1 --> D1["forgot unlock!"] end subgraph With Lock Guard A2["create guard"] --> B2["auto lock"] B2 --> C2["work"] C2 --> D2["crash?"] D2 --> E2["auto unlock!"] end

Condition Variables: The Waiting Room

The Restaurant Analogy

You order food at a restaurant. You don’t stand at the kitchen door asking “Is it ready? Is it ready?” every second.

Instead, you sit and wait. The waiter notifies you when food is ready.

What Condition Variables Do

  • Thread can wait efficiently (sleep)
  • Another thread can wake it up
  • No busy waiting = No wasted energy

Example: Producer-Consumer

#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool foodReady = false;

void customer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{
        return foodReady;
    });
    // Eat food!
}

void chef() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        foodReady = true;
    }
    cv.notify_one(); // "Order up!"
}

How It Works

graph TD A["Customer arrives"] --> B["Sits and waits"] B --> C["Chef cooking..."] C --> D["Chef: Food ready!"] D --> E["Chef notifies customer"] E --> F["Customer wakes up"] F --> G["Customer eats"]

Three Key Methods

Method What it does
wait() Sleep until notified
notify_one() Wake up ONE waiter
notify_all() Wake up ALL waiters

Deadlock: The Mexican Standoff

What is Deadlock?

Imagine two kids:

  • Kid A holds red crayon, wants blue
  • Kid B holds blue crayon, wants red
  • Neither will share first
  • Both wait forever!

Deadlock in Code

std::mutex mutexA, mutexB;

void thread1() {
    mutexA.lock();    // Got A!
    // tiny delay
    mutexB.lock();    // Want B...
    // Work
    mutexB.unlock();
    mutexA.unlock();
}

void thread2() {
    mutexB.lock();    // Got B!
    // tiny delay
    mutexA.lock();    // Want A...
    // Work
    mutexA.unlock();
    mutexB.unlock();
}
// Both threads stuck!

The Deadlock Diagram

graph LR T1["Thread 1"] -->|holds| A["Mutex A"] T1 -->|wants| B["Mutex B"] T2["Thread 2"] -->|holds| B T2 -->|wants| A style A fill:#ff6b6b style B fill:#4ecdc4

Deadlock Prevention: The Four Rules

Rule 1: Lock Ordering

Always lock in the same order!

// GOOD: Both threads lock A then B
void thread1() {
    mutexA.lock();
    mutexB.lock();
    // work
}

void thread2() {
    mutexA.lock();  // Same order!
    mutexB.lock();
    // work
}

Rule 2: Use std::lock

Lock multiple mutexes at once!

void safeFunction() {
    std::lock(mutexA, mutexB);
    std::lock_guard<std::mutex>
        lockA(mutexA, std::adopt_lock);
    std::lock_guard<std::mutex>
        lockB(mutexB, std::adopt_lock);
    // Safe!
}

Rule 3: Use scoped_lock (C++17)

The easiest way!

void modernWay() {
    std::scoped_lock lock(mutexA, mutexB);
    // Both locked safely
    // Both unlock automatically
}

Rule 4: Don’t Hold Locks Long

Lock late, unlock early

void goodHabit() {
    // Do prep work first
    prepareData();

    {
        std::lock_guard<std::mutex> lock(m);
        // Quick operation only
        sharedData = newValue;
    } // Unlock immediately

    // Continue other work
    processResults();
}

Quick Summary: Your Synchronization Toolkit

Problem Solution Remember
Race Condition Mutex “One at a time”
Forgetting unlock Lock Guard “Auto-close door”
Busy waiting Condition Variable “Restaurant bell”
Deadlock Lock ordering / scoped_lock “Same order always”

The Golden Rules

  1. Protect shared data with a mutex
  2. Use lock_guard instead of manual lock/unlock
  3. Wait efficiently with condition variables
  4. Lock in consistent order to prevent deadlock
  5. Keep locked sections short and simple

Real Life Examples

Bank Account Transfer

void transfer(Account& from,
              Account& to,
              int amount) {
    std::scoped_lock lock(
        from.mutex, to.mutex);
    from.balance -= amount;
    to.balance += amount;
}
// Safe transfer between
// any two accounts!

Print Queue

std::queue<std::string> printJobs;
std::mutex queueMutex;
std::condition_variable hasJobs;

void addJob(std::string doc) {
    {
        std::lock_guard<std::mutex>
            lock(queueMutex);
        printJobs.push(doc);
    }
    hasJobs.notify_one();
}

void printer() {
    while (true) {
        std::unique_lock<std::mutex>
            lock(queueMutex);
        hasJobs.wait(lock, []{
            return !printJobs.empty();
        });
        auto job = printJobs.front();
        printJobs.pop();
        lock.unlock();
        print(job);
    }
}

You Did It!

You now understand:

  • Race Conditions - The cookie jar problem
  • Mutexes - The bathroom lock
  • Lock Guards - The automatic door
  • Condition Variables - The restaurant bell
  • Deadlock Prevention - The ordering rules

These are the building blocks of safe, fast, concurrent programs!

Remember: Synchronization is like traffic rules. Follow them, and everyone gets where they’re going safely.

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.