C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
107: Mutexes and Locking Strategies
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_fundsfunction to ensure that bothAccount` mutexes are acquired safely. - Do not use manual
.lock()or.unlock()calls. - Ensure the solution handles cases where
Account Atransfers toAccount BwhileAccount Btransfers toAccount Asimultaneously.
#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;
}
There are no comments for now.