Skip to Content
Course content

107: Mutexes and Locking Strategies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing code from engineers moving into systems programming: they treat a std::mutex like a magic shield that they can "wrap" around a variable. They think that by simply declaring a mutex next to a piece of data, that data is now magically protected from concurrent access.

The Myth of the Magic Shield vs. Mutexes as Social Contracts

Here is the reality: a mutex does not protect data; it protects code. There is no physical or compiler-enforced link between a std::mutex and the variables you intend to guard with it. If you have a shared std::vector and a mutex, but one rogue thread decides to write to that vector without locking the mutex first, the mutex won't stop it. It doesn't "lock" the memory; it just provides a flag that other threads can check.

// This is a common mistake.
std::vector<int> shared_log;
std::mutex log_mutex;

void thread_A() {
    std::lock_guard<std::mutex> lock(log_mutex);
    shared_log.push_back(1); // Safe... as long as everyone else plays along.
}

void thread_B() {
    // I forgot to lock! 
    // The mutex doesn't "know" shared_log is its responsibility.
    shared_log.push_back(2); // CRASH: Data race and potential heap corruption.
}

In the example above, thread_B doesn't care that log_mutex exists. The mutex is essentially a "social contract" between your threads. You are agreeing as a developer that "nobody touches shared_log unless they hold log_mutex." If one thread breaks the contract, the whole system collapses.

Stop Calling .lock() and .unlock() Manually

When you're starting out, it's tempting to just call mtx.lock() at the start of a function and mtx.unlock() at the end. Please, don't do this. If your code throws an exception or returns early via an if statement before it hits that unlock call, your mutex stays locked forever. You've just created a deadlock that will freeze your entire application.

Instead, use RAII (Resource Acquisition Is Initialization). In C++, this means std::lock_guard for simple cases and std::unique_lock when you need more flexibility (like manually unlocking or using condition variables). I almost always default to std::lock_guard because it's lightweight and impossible to forget to unlock—it happens automatically when the object goes out of scope.

void safe_log(int value) {
    // Lock is acquired here.
    std::lock_guard<std::mutex> lock(log_mutex); 
    shared_log.push_back(value);
    // Lock is automatically released here, even if push_back throws an exception.
} 

The Deadlock Dance and How to Avoid It

As your project grows, you'll inevitably need more than one mutex. This is where things get dangerous. Imagine Thread A locks Mutex 1 and waits for Mutex 2. Simultaneously, Thread B locks Mutex 2 and waits for Mutex 1. They are now staring at each other in a permanent stalemate. I've spent way too many late nights debugging these because they are non-deterministic; they might happen once every thousand runs.

The old-school fix was to always lock mutexes in the exact same order across the entire codebase. That's a nightmare to maintain. The modern C++ solution is std::scoped_lock (introduced in C++17). It can take multiple mutexes and uses a deadlock-avoidance algorithm to lock all of them at once.

void transfer_funds(Account& from, Account& to, double amount) {
    // Locks both accounts without risking a deadlock, 
    // regardless of which account was passed as 'from' or 'to'.
    std::scoped_lock lock(from.mtx, to.mtx);
    
    from.balance -= amount;
    to.balance += amount;
} 



📋 Practical Task

Fixing the Deadlocked Bank Transfer System

You have been handed a piece of legacy code for a banking system. It contains a critical bug: whenever two users transfer money to each other at the exact same time, the system intermittently freezes (deadlocks). Your task is to fix the transfer_funds function to be thread-safe and deadlock-free.

Requirements:

  • Modify the transfer_funds function to ensure that both Account` mutexes are acquired safely.
  • Do not use manual .lock() or .unlock() calls.
  • Ensure the solution handles cases where Account A transfers to Account B while Account B transfers to Account A simultaneously.
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

struct Account {
    int id;
    double balance;
    std::mutex mtx;

    Account(int i, double b) : id(i), balance(b) {}
};

void transfer_funds(Account& from, Account& to, double amount) {
    // TODO: Fix this section to prevent deadlocks
    // Current broken implementation:
    // std::lock_guard<std::mutex> lock1(from.mtx);
    // std::lock_guard<std::mutex> lock2(to.mtx);
    
    from.balance -= amount;
    to.balance += amount;
}

int main() {
    Account acc1(1, 1000.0);
    Account acc2(2, 1000.0);

    // Simulate concurrent transfers that cause deadlock
    std::thread t1([&]() {
        for(int i=0; i<1000; ++i) transfer_funds(acc1, acc2, 1.0);
    });
    std::thread t2([&]() {
        for(int i=0; i<1000; ++i) transfer_funds(acc2, acc1, 1.0);
    });

    t1.join();
    t2.join();

    std::cout < "Final balances: " < acc1.balance < ", " < acc2.balance < std::endl;
    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.