网站备案当面核验,怎么查看网站有没有做竞价,开题报告风景区网站开发,微信公众号网页版入口文章目录 场景描述#x1f41e; 初始实现: 非线程安全版本互斥锁: std::mutex使用mutex保护共享资源使用std::lock_guard简化锁的管理 优化读操作: std::shared_mutex多个锁的管理: std::scoped_lock使用std::scoped_lock避免死锁 其他高级锁⏳ 带超时的锁: std::timed_mutex使… 文章目录 场景描述 初始实现: 非线程安全版本互斥锁: 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 总结 自从 C11 在语言层面引入多线程模型之后, C 标准库提供了一套完整的工具, 用于实现线程同步, 防止多个线程同时访问共享资源时出现的数据竞争. 这些工具包括了基本的互斥锁
std::mutex 以及基于
RAII 的锁管理器, 如
std::lock_guard, 极大简化了开发者处理并发问题的复杂度. 在后续的标准更新中, C 又陆续引入了更多高级的同步机制: C14: 引入 std::shared_mutex, 支持多读单写的场景优化读性能. C17: 新增 std::shared_lock 和 std::scoped_lock, 分别用于管理读锁生命周期和防止多锁死锁问题.
为了让初学者更直观地理解这些锁的应用场景, 本文以银行账户管理系统为例, 逐步介绍现代 C 中不同锁的特点和使用方法. 场景描述
设计一个银行账户类, 可以执行以下操作:
存款(Deposit)取款(Withdraw)查询余额(Get Balance)
另外需要保证这个类是线程安全的, 即多个线程可以同时对账户进行存款和取款操作, 但是不会出现数据竞争. 初始实现: 非线程安全版本
首先实现一个最基础的版本, 这个版本没有考虑多线程的问题, 多次运行这个程序就会发现最后的输出结果不会一直是 0, 这是因为多个线程对共享资源balance的操作存在不确定性.
#include iostream
#include threadclass 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_guardstd::mutex lock(mutex_);balance_ amount;
}修改后的代码如下:
#include mutexclass BankAccount {public:// 存款void Deposit(int amount) {std::lock_guardstd::mutex lock(mutex_);balance_ amount;}// 取款void Withdraw(int amount) {std::lock_guardstd::mutex lock(mutex_);balance_ - amount;}// 查询余额int GetBalance() {std::lock_guardstd::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_guardstd::shared_mutex lock(mutex_);balance_ - amount;
}读取操作可以这样写:
int GetBalance() {// 可以有多个线程同时获得锁std::shared_lockstd::shared_mutex lock(mutex_);return balance_;
}如下是修改后的代码:
#include iostream
#include mutex
#include shared_mutex
#include threadclass BankAccount {public:// 存款void Deposit(int amount) {std::unique_lockstd::shared_mutex lock(mutex_);balance_ amount;}// 取款void Withdraw(int amount) {std::unique_lockstd::shared_mutex lock(mutex_);balance_ - amount;}// 查询余额int GetBalance() {std::shared_lockstd::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避免死锁
为了避免死锁, C17 中引入了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可以用在下面这些场景:
递归函数中的锁保护在同一线程中调用多个依赖相同锁的函数复杂逻辑流程中需要多次加锁
但是作者认为, 在实际开发中, 应该尽量避免使用std::recursive_mutex, 因为它会增加代码的复杂性, 并且容易引入死锁的风险. 而且如果一个函数需要多次加锁, 可能意味着这个函数的设计不够合理.
#include iostream
#include mutex
#include threadstd::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::mutex 和 std::lock_guard: 基础的互斥锁保护std::shared_mutex 和 std::shared_lock: 适用于多读单写的优化场景std::scoped_lock: 解决多锁管理中的死锁问题
希望本文能帮助你更好地理解 C 中的锁机制, 在实际开发中灵活选择合适的工具.