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
108: Lock Guards and Unique Locks
I've seen this bug more times than I care to admit in production code: a mutex is locked at the start of a function, but because of a poorly placed return statement or an unexpected exception, the unlock() call is skipped. The result is a deadlock that doesn't happen during your quick local tests, but triggers only under heavy load in a live environment, freezing the entire system. When you're managing locks manually, you're essentially betting that you'll never make a logic error in your control flow. That's a bet I'm not willing to take.
The Danger of Manual Unlocking
Let's look at a classic example. Imagine we're writing a simple thread-safe bank account. You might be tempted to do something like this:
void Account::withdraw(double amount) {
mtx.lock();
if (amount > balance) {
mtx.unlock(); // You have to remember this!
throw std::runtime_error("Insufficient funds");
}
balance -= amount;
mtx.unlock(); // And this!
}
On the surface, this looks correct. But it's fragile. If you add a new validation check and forget the unlock() call before the return or throw, you've just locked that account forever. Even worse, if balance -= amount somehow throws an exception (unlikely here, but common in more complex objects), the mutex stays locked. You're forcing yourself to manage the state of the lock manually across every possible exit path of the function. It's tedious and error-prone.
Letting the Destructor Do the Heavy Lifting
The better way is to use RAII (Resource Acquisition Is Initialization). In C++, we have std::lock_guard for exactly this purpose. Instead of calling lock and unlock, you create a local object that manages the mutex for you. When that object goes out of scope—whether via a return, an exception, or just reaching the end of the block—its destructor automatically releases the mutex.
void Account::withdraw(double amount) {
std::lock_guard<std::mutex> lock(mtx);
if (amount > balance) {
throw std::runtime_error("Insufficient funds"); // Mutex unlocks automatically!
}
balance -= amount;
} // Mutex unlocks automatically here too!
I love lock_guard because it's "set it and forget it." It's incredibly lightweight—essentially just a wrapper around the lock/unlock calls—and it guarantees that you can't leak a lock. However, lock_guard is intentionally rigid. Once it's locked, it stays locked until the end of the scope. You can't manually unlock it early to perform a time-consuming I/O operation that doesn't actually need the lock.
When You Need More Control than a Guard Provides
This is where std::unique_lock comes in. Think of it as the "power user" version of lock_guard. It provides the same RAII safety—it will still unlock in the destructor—but it gives you the flexibility to call .unlock()` and .lock()` manually whenever you want.
Consider a scenario where you need to lock a mutex, update some data, and then perform a slow network call. You don't want to hold the lock during the network call because you'd be blocking every other thread from accessing the account.
void Account::processComplexTransaction(double amount) {
std::unique_lock<std::mutex> lock(mtx);
balance -= amount;
// We've updated the state, now we can release the lock
// while we do something slow and unrelated to the mutex.
lock.unlock();
networkClient.sendTransactionLog(amount);
// If we need to update the status afterward, we can relock.
lock.lock();
lastTransactionTime = std::time(nullptr);
}
The trade-off here is a tiny bit of performance. unique_lock has to store a flag internally to track whether it currently owns the mutex (since it can be unlocked and relocked), whereas lock_guard doesn't. In 99% of application code, this overhead is negligible. I generally tell my colleagues: use lock_guard by default for its simplicity and speed, but reach for unique_lock the moment you need manual control or if you're working with std::condition_variable, which requires it.
📋 Practical Task
Refactoring a Deadlocking Transaction Manager
You have been handed a legacy TransactionManager class that is suffering from intermittent deadlocks. The current implementation uses manual .lock()` and .unlock()` calls, and there is a bug where an exception during a transaction prevents the mutex from ever being released.
Your Task:
Refactor the following code to eliminate the possibility of deadlocks using std::lock_guard or std::unique_lock. You must ensure that the mutex is released regardless of whether the transaction succeeds or throws an exception.
#include <mutex>
#include <stdexcept>
#include <vector>
class TransactionManager {
std::mutex mtx;
std::vector<int> ledger;
public:
void addEntry(int value) {
mtx.lock();
if (value < 0) {
// BUG: The mutex is not unlocked before throwing!
throw std::invalid_argument("Negative values not allowed");
}
ledger.push_back(value);
mtx.unlock();
}
void clearLedger() {
mtx.lock();
ledger.clear();
mtx.unlock();
}
};
Requirements:
- Replace the manual locking mechanism with the most appropriate RAII wrapper.
- Ensure the code remains thread-safe.
- The solution should handle the exception case without leaving the mutex in a locked state.
There are no comments for now.