文章目录
C++ 并发编程:深入理解 std::mutex
和 std::lock_guard
一、为什么需要互斥锁?
在多线程程序中,当多个线程同时访问共享资源时,可能会引发数据竞争(Data Race)问题。比如两个线程同时修改同一个全局变量:
int counter = 0; // 共享资源
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 非原子操作
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
std::cout << "Final counter: " << counter; // 可能输出小于200000
运行结果可能是不可预测的,因为 counter++ 不是原子操作。此时需要互斥锁(Mutex)来保证操作的原子性。
二、std::mutex 基本用法
std::mutex
是 C++11 提供的互斥锁类,通过 lock()
和 unlock()
方法手动控制锁。
修正后的线程安全代码:
include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
counter++;
mtx.unlock(); // 必须显式解锁
}
// 最终输出一定是200000
手动锁的痛点:
- 忘记解锁会导致死锁。
- 异常发生时可能跳过解锁步骤。
三、std::lock_guard:自动锁管理
std::lock_guard
是一个 RAII 包装类,构造时自动加锁,析构时自动解锁,确保异常安全。
使用示例:
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
counter++;
// 离开作用域时自动解锁
}
核心特性:
- 自动管理生命周期:无需手动调用
unlock()
。 - 异常安全:即使代码抛出异常,锁也会被释放。
- 不可复制:确保锁的所有权唯一。
四、lock_guard 的底层原理
简化实现代码:
template <typename Mutex>
class lock_guard {
public:
explicit lock_guard(Mutex& m) : mutex(m) {
mutex.lock(); // 构造时加锁
~lock_guard() {
mutex.unlock(); // 析构时解锁
// 禁止复制
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
Mutex& mutex;
};
五、实际应用场景
案例1:线程安全的银行账户
class BankAccount {
private:
std::mutex mtx;
double balance;
public:
void deposit(double amount) {
std::lock_guard<std::mutex> lock(mtx);
balance += amount;
void withdraw(double amount) {
std::lock_guard<std::mutex> lock(mtx);
if (balance >= amount) {
balance -= amount;
}
};
案例2:保护文件写入
std::mutex log_mutex;
void write_log(const std::string& message) {
std::lock_guard<std::mutex> lock(log_mutex);
std::ofstream file("app.log", std::ios::app);
file << message << std::endl;
// 文件流和锁都会自动释放
六、注意事项
- 锁的粒度:
// 错误:锁范围过大,影响性能
std::lock_guard<std::mutex> lock(mtx);
{
data1.process();
data2.process(); // 这两个操作可能不需要共享锁
}
// 正确:细化锁范围
{
std::lock_guard<std::mutex> lock1(mtx1);
data1.process();
}
{
std::lock_guard<std::mutex> lock2(mtx2);
data2.process();
}
- 避免嵌套死锁:
std::mutex mtx;
void foo() {
std::lock_guard<std::mutex> lock(mtx);
bar(); // 如果bar()也申请同一个锁,导致死锁
}
void bar() {
std::lock_guard<std::mutex> lock(mtx); // 死锁!
}
- 配合其他锁类型:
- std::unique_lock:更灵活(延迟锁定、转移所有权)。
- std::shared_mutex:读写分离锁(C++17)。
七、常见问题解答
Q1:lock_guard 和 unique_lock 有什么区别?
特性 | lock_guard | unique_lock |
---|---|---|
锁策略 | 必须立即锁定 | 可延迟锁定 (defer_lock) |
所有权转移 | 不支持 | 支持 |
性能 | 更高(无额外开销) | 稍低(更多功能) |
适用场景 | 简单作用域锁定 | 需要灵活控制锁的情况 |
Q2:如何选择锁的类型?
- 简单场景用 lock_guard(90% 的情况)。
- 需要条件变量或延迟锁定用 unique_lock。
八、总结
- std::mutex:基础互斥锁,需手动管理锁生命周期。
- std::lock_guard:
- 自动管理锁,确保异常安全。
- 适用于简单的作用域锁定。
- 代码更简洁,避免忘记解锁。
最佳实践:
{
std::lock_guard<std::mutex> lock(mtx); // 进入作用域加锁
// 操作共享资源...
} // 离开作用域自动解锁