欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > 现代C++锁介绍

现代C++锁介绍

2025/4/19 4:14:59 来源:https://blog.csdn.net/arong_xu/article/details/144704754  浏览:    关键词:现代C++锁介绍

文章目录

    • 场景描述
    • 🐞 初始实现: 非线程安全版本
    • 互斥锁: `std::mutex`
      • 使用`mutex`保护共享资源
      • 使用`std::lock_guard`简化锁的管理
    • 优化读操作: `std::shared_mutex`
    • 多个锁的管理: `std::scoped_lock`
      • 使用`std::scoped_lock`避免死锁
    • 其他高级锁
      • ⏳ 带超时的锁: `std::timed_mutex`
        • 使用场景
        • 使用`std::timed_mutex`实现超时锁
      • 支持多次锁定的锁: `std::recursive_mutex`
        • 使用场景
      • 带超时和可重入的锁: `std::recursive_timed_mutex`
    • 📅 总结

自从 C++11 在语言层面引入多线程模型之后, C++ 标准库提供了一套完整的工具, 用于实现线程同步, 防止多个线程同时访问共享资源时出现的数据竞争. 这些工具包括了基本的互斥锁 std::mutex 以及基于 RAII 的锁管理器, 如 std::lock_guard, 极大简化了开发者处理并发问题的复杂度.

在后续的标准更新中, C++ 又陆续引入了更多高级的同步机制:

  • C++14: 引入 std::shared_mutex, 支持多读单写的场景优化读性能.

  • C++17: 新增 std::shared_lockstd::scoped_lock, 分别用于管理读锁生命周期和防止多锁死锁问题.

为了让初学者更直观地理解这些锁的应用场景, 本文以银行账户管理系统为例, 逐步介绍现代 C++ 中不同锁的特点和使用方法.


场景描述

设计一个银行账户类, 可以执行以下操作:

  1. 存款(Deposit)
  2. 取款(Withdraw)
  3. 查询余额(Get Balance)

另外需要保证这个类是线程安全的, 即多个线程可以同时对账户进行存款和取款操作, 但是不会出现数据竞争.


🐞 初始实现: 非线程安全版本

首先实现一个最基础的版本, 这个版本没有考虑多线程的问题, 多次运行这个程序就会发现最后的输出结果不会一直是 0, 这是因为多个线程对共享资源balance的操作存在不确定性.

#include <iostream>
#include <thread>class BankAccount {public:// 存款void Deposit(int amount) { balance_ += amount; }// 取款void Withdraw(int amount) { balance_ -= amount; }// 查询余额int GetBalance() { return balance_; }private:int balance_ = 0;
};int main() {BankAccount account;std::thread t1([&account] { account.Deposit(100); });std::thread t2([&account] { account.Withdraw(100); });t1.join();t2.join();std::cout << "Balance: " << account.GetBalance() << std::endl;return 0;
}

互斥锁: std::mutex

使用mutex保护共享资源

多个线程可能同时对账户进行存款和取款操作, 为了防止数据竞争, 需要使用 std::mutex 确保同时只有一个线程可以修改账户余额.

为此我们引入一个类成员变量std::mutex, 并在每个操作前后加锁和解锁.

比如Deposit函数可以改为:

void Deposit(int amount) {mutex.lock();balance += amount;mutex.unlock();
}

使用std::lock_guard简化锁的管理

当前例子中被锁保护的代码比较简单, 不会发生异常. 但是实际工作中往往会遇到被锁保护的代码中可能会发生异常的情况, 或者有return语句, 这样会导致锁无法被释放, 从而引发死锁.

void foo(int amount) {mutex.lock();if (amount < 0) {mutex.unlock();return; // 早期返回}bar(); // 可能会抛出异常mutex.unlock();
}

为了解决这个问题, 我们可以使用std::lock_guard来保证在函数退出时mutex一定会被解锁.
std::lock_guard是一个 RAII 风格的类, 它在构造时会锁定mutex, 在析构时会解锁mutex.

void Deposit(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ += amount;
}

修改后的代码如下:

#include <mutex>class BankAccount {public:// 存款void Deposit(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ += amount;}// 取款void Withdraw(int amount) {std::lock_guard<std::mutex> lock(mutex_);balance_ -= amount;}// 查询余额int GetBalance() {std::lock_guard<std::mutex> lock(mutex_);return balance_;}private:int balance_ = 0;std::mutex mutex_;
};

优化读操作: std::shared_mutex

在上面的实现中, 查询余额是只读操作, 使用互斥锁会阻塞其他线程的读操作. 为提升性能, 可引入 std::shared_mutex 实现多读单写:

std::shared_mutex是一个读写锁, 它支持两种操作:

  • 读操作: 使用(std::shared_lock), 多个线程可以同时获取读锁, 但不能获取写锁. 读锁的获取不会阻塞其他读锁的获取.
  • 写操作: 使用(std::lock_guard), 只有一个线程可以获取写锁, 且此时不能有其他线程获取读锁或写锁.

改进后的代码:

void Withdraw(int amount) {// 最多只能有一个线程获得锁std::lock_guard<std::shared_mutex> lock(mutex_);balance_ -= amount;
}

读取操作可以这样写:

int GetBalance() {// 可以有多个线程同时获得锁std::shared_lock<std::shared_mutex> lock(mutex_);return balance_;
}

如下是修改后的代码:

#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>class BankAccount {public:// 存款void Deposit(int amount) {std::unique_lock<std::shared_mutex> lock(mutex_);balance_ += amount;}// 取款void Withdraw(int amount) {std::unique_lock<std::shared_mutex> lock(mutex_);balance_ -= amount;}// 查询余额int GetBalance() {std::shared_lock<std::shared_mutex> lock(mutex_);return balance_;}private:int balance_ = 0;std::shared_mutex mutex_;
};

多个锁的管理: std::scoped_lock

考虑在上述BankAccount类中增加一个转账的操作Transfer, 转账操作需要同时锁定两个账户的锁. 如果直接使用std::lock来锁定两个锁, 可能会出现死锁的情况.

错误的写法如下:

void Transfer(BankAccount& to, int amount) {mutex_.lock();    // 错误写法to.mutex_.lock(); // 错误写法balance_ -= amount;to.balance_ += amount;
}

为什么会出错呢, 考虑下面的使用场景:

BankAccount a, b;std::jthread t1([&a, &b] { a.Transfer(b, 100); });
std::jthread t2([&a, &b] { b.Transfer(a, 100); });
  • t1线程会锁定a的锁, 然后尝试锁定b的锁
  • t2线程会锁定b的锁, 然后尝试锁定a的锁

加锁的顺序不固定会导致出现死锁的情况.

使用std::scoped_lock避免死锁

为了避免死锁, C++17 中引入了std::scoped_lock, 它可以同时锁定多个锁, 并且避免死锁的情况.

std::scoped_lock是一个 RAII 风格的类, 它在构造时会锁定多个mutex, 按照一个固定顺序去锁定, 在析构时会解锁这些mutex.

void Transfer(BankAccount& to, int amount) {std::scoped_lock lock(mutex_, to.mutex_); // 正确写法balance_ -= amount;to.balance_ += amount;
}

其他高级锁

⏳ 带超时的锁: std::timed_mutex

使用场景

在某些情况下, 我们可能需要在一段时间内尝试获取锁, 如果超时则放弃获取锁. 这种情况下可以使用std::timed_mutex.

std::timed_mutex支持为锁定操作设置超时时间, 可以用try_lock_for()try_lock_until()来尝试获取锁.

使用std::timed_mutex实现超时锁
if (mutex.try_lock_for(std::chrono::seconds(2))) {       // 尝试获取锁, 超时时间为2秒mutex.unlock();
} else {std::cout << "Failed to acquire lock for withdrawal within timeout.\n";
}

支持多次锁定的锁: std::recursive_mutex

使用场景

理论上std::recursive_mutex可以用在下面这些场景:

  1. 递归函数中的锁保护
  2. 在同一线程中调用多个依赖相同锁的函数
  3. 复杂逻辑流程中需要多次加锁

但是作者认为, 在实际开发中, 应该尽量避免使用std::recursive_mutex, 因为它会增加代码的复杂性, 并且容易引入死锁的风险. 而且如果一个函数需要多次加锁, 可能意味着这个函数的设计不够合理.

#include <iostream>
#include <mutex>
#include <thread>std::recursive_mutex rmutex;void recursive_function(int count) {if (count <= 0) return;rmutex.lock();std::cout << "Thread " << std::this_thread::get_id()<< " acquired lock at count " << count << std::endl;recursive_function(count - 1);rmutex.unlock();
}int main() {std::jthread t1(recursive_function, 5);std::jthread t2(recursive_function, 5);return 0;
}

带超时和可重入的锁: std::recursive_timed_mutex

是前面二者的集合体.

📅 总结

现代 C++ 提供了丰富的并发工具, 通过不同种类的锁机制, 开发者可以轻松应对复杂的多线程场景. 本文以银行账户管理系统为例, 详细阐述了以下锁的使用场景:

  • std::mutexstd::lock_guard: 基础的互斥锁保护
  • std::shared_mutexstd::shared_lock: 适用于多读单写的优化场景
  • std::scoped_lock: 解决多锁管理中的死锁问题

希望本文能帮助你更好地理解 C++ 中的锁机制, 在实际开发中灵活选择合适的工具. 🚀

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词