当前位置: 首页 > web >正文

C++并发编程指南 递归锁 介绍

文章目录

      • 什么是递归锁?
      • 核心特性:锁的计数器
      • 为什么需要递归锁?一个经典场景
      • C++ 中的递归锁:`std::recursive_mutex` 和 `std::recursive_timed_mutex`
      • 递归锁的注意事项和缺点
      • 总结
  • 递归锁(Recursive Lock)完整示例
    • 场景描述
    • 完整代码示例
    • 代码解析
      • 1. 嵌套调用示例 - `transfer` 方法
      • 2. 递归函数示例 - `calculate_compound_interest` 方法
      • 3. 多线程安全性
    • 输出示例
    • 重要注意事项
      • 为什么需要重复获取相同的锁
  • 重构代码逻辑避免递归锁使用
  • 重构 BankAccount 类避免递归锁使用
    • 重构策略说明
      • 1. 提取不加锁的私有方法
      • 2. 重构 `transfer()` 方法
      • 3. 重构 `calculate_compound_interest()` 方法
    • 优点
    • 注意事项

好的,我们来详细介绍一下递归锁(Recursive Lock),在 C++ 中,它的典型代表是 std::recursive_mutex

什么是递归锁?

递归锁是一种特殊的互斥量,它允许同一个线程多次对其加锁,而不会产生死锁。

普通互斥量(如 std::mutex)如果被同一个线程多次加锁,会导致未定义行为(通常是立即死锁——线程自己把自己卡住了)。递归锁则专门为了解决“一个线程需要多次进入同一个临界区”的场景而设计。

核心特性:锁的计数器

递归锁内部不仅维护了一个“锁定/未锁定”的状态,还维护了一个锁计数器和一个线程所有者ID

  1. 当线程第一次成功获取递归锁时,锁被标记为已锁定,并记录所有者线程ID,计数器设为 1。
  2. 同一个线程再次请求锁时,计数器简单地递增(例如,变为 2),线程继续执行。
  3. 每次调用 unlock() 时,计数器递减
  4. 只有当计数器减回到 0 时,锁才会被真正释放,从而允许其他线程获取它。

为什么需要递归锁?一个经典场景

递归锁最常见的使用场景是:递归函数可重入函数

假设有一个函数,它需要锁住一个资源才能工作,而这个函数又会被另一个也需要锁住同一资源的函数调用。

没有递归锁(使用 std::mutex)会导致死锁:

#include <iostream>
#include <mutex>std::mutex mtx;void bar() {std::lock_guard<std::mutex> lock(mtx); // 尝试再次获取锁 -> 死锁!// ... 做一些事情 ...
}void foo() {std::lock_guard<std::mutex> lock(mtx); // 第一次获取锁成功bar(); // 在 bar() 内部,同一个线程试图再次获取同一个锁
}int main() {foo(); // 导致死锁return 0;
}

在上面的代码中,foo() 先锁住了 mtx,然后调用 bar()bar() 也试图锁住同一个 mtx。对于普通的 std::mutex,这是不允许的,线程会在 bar() 中尝试获取锁时永远阻塞,因为它已经在 foo() 中拥有了这个锁,从而造成死锁。

使用递归锁(std::recursive_mutex)解决问题:

#include <iostream>
#include <mutex> // 注意:std::recursive_mutex 也在 <mutex> 头文件中std::recursive_mutex rec_mtx; // 使用递归锁void bar() {std::lock_guard<std::recursive_mutex> lock(rec_mtx); // 计数器+1 (现在是2)std::cout << "Inside bar()" << std::endl;// 函数结束,lock 析构,计数器-1 (现在是1)
}void foo() {std::lock_guard<std::::recursive_mutex> lock(rec_mtx); // 第一次获取锁,计数器=1std::cout << "Inside foo()" << std::endl;bar(); // 成功调用,不会死锁// 函数结束,lock 析构,计数器-1 (现在是0),锁被真正释放
}int main() {foo();std::cout << "Execution completed successfully." << std::endl;return 0;
}

这段代码可以正常工作并成功退出。输出:

Inside foo()
Inside bar()
Execution completed successfully.

C++ 中的递归锁:std::recursive_mutexstd::recursive_timed_mutex

C++ 标准库提供了两种递归锁:

  1. std::recursive_mutex

    • 基本的递归互斥量。
    • 成员函数和 std::mutex 一样:lock(), try_lock(), unlock()
  2. std::recursive_timed_mutex

    • 带超时功能的递归互斥量。
    • 除了 lock(), try_lock(), unlock(),还提供了 try_lock_for()try_lock_until(),允许线程尝试在指定时间内获取锁。

管理类:
与普通互斥量一样,推荐使用 RAII 包装器来管理递归锁,以避免忘记解锁:

  • std::lock_guard<std::recursive_mutex>
  • std::unique_lock<std::recursive_mutex> (更灵活,可手动解锁)

递归锁的注意事项和缺点

虽然递归锁解决了特定问题,但通常被认为是一种“代码异味”(Code Smell),应谨慎使用。

  1. 逻辑复杂性:需要递归锁的设计往往意味着你的代码结构可能不够清晰。函数之间的调用关系变得复杂,锁的持有时间也变长了(从第一次加锁到最后一次解锁),这可能会降低性能。
  2. 容易出错:你必须确保 解锁的次数完全等于加锁的次数。如果少解锁一次,锁就永远不会被释放;如果多解锁一次,会导致未定义行为。
  3. 掩盖设计问题:有时,使用递归锁只是掩盖了更深层次的设计缺陷。更好的方法可能是:
    • 重构代码:将需要加锁的公共部分提取成一个新的私有函数,这个函数由原函数调用,但自身不加锁(需要调用者保证已持有锁)。这样公共部分只需要加一次锁。
    • 使用不可重入的设计:重新思考函数职责,避免在已持有锁的情况下调用另一个也需要相同锁的函数。

总结

特性普通锁 (e.g., std::mutex)递归锁 (e.g., std::recursive_mutex)
同一线程重复加锁导致未定义行为/死锁允许,内部计数器递增
性能通常更高稍低(需要维护计数器和管理所有者线程)
使用场景绝大多数常规的临界区保护递归函数、复杂回调、难以重构的遗留代码
推荐度首选慎用,作为最后手段

核心建议: 首先优先考虑代码设计,尽量避免需要递归锁的场景。如果确实无法避免(例如第三方库的回调要求),再使用 std::recursive_mutex,并且要非常小心地管理锁的获取和释放次数。

递归锁(Recursive Lock)完整示例

下面我将通过一个模拟"银行账户"的场景来演示递归锁的使用。这个例子中,我们会看到两种需要递归锁的情况:递归函数调用和嵌套调用。

场景描述

假设我们有一个BankAccount类,它包含:

  1. 基本操作:存款(deposit)、取款(withdraw)、查询余额(get_balance)
  2. 复杂操作:转账(transfer),它内部会调用取款和存款
  3. 利息计算(apply_interest),这是一个递归计算复利的函数

完整代码示例

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>class BankAccount {
private:double balance_;mutable std::recursive_mutex mtx_; // 使用递归锁public:BankAccount(double initial_balance = 0) : balance_(initial_balance) {}// 存款void deposit(double amount) {std::lock_guard<std::recursive_mutex> lock(mtx_);balance_ += amount;std::cout << "Deposited: " << amount << ", New Balance: " << balance_ << std::endl;}// 取款bool withdraw(double amount) {std::lock_guard<std::recursive_mutex> lock(mtx_);if (balance_ >= amount) {balance_ -= amount;std::cout << "Withdrew: " << amount << ", New Balance: " << balance_ << std::endl;return true;}std::cout << "Failed to withdraw: " << amount << ", Insufficient balance: " << balance_ << std::endl;return false;}// 查询余额double get_balance() const {std::lock_guard<std::recursive_mutex> lock(mtx_);return balance_;}// 转账 - 会调用withdraw和deposit,形成嵌套锁bool transfer(BankAccount& to_account, double amount) {std::lock_guard<std::recursive_mutex> lock(mtx_);std::cout << "Attempting transfer of: " << amount << std::endl;if (this->withdraw(amount)) { // 这里会再次获取锁(递归锁)to_account.deposit(amount); // 这里会获取to_account的锁std::cout << "Transfer successful!" << std::endl;return true;}std::cout << "Transfer failed!" << std::endl;return false;}// 计算复利 - 递归函数,需要递归锁double calculate_compound_interest(double rate, int years) const {std::lock_guard<std::recursive_mutex> lock(mtx_);if (years <= 0) {return balance_;}// 模拟复杂计算过程std::cout << "Calculating interest for year " << years << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));// 递归调用 - 这里会再次获取同一个锁(递归锁)double future_balance = calculate_compound_interest(rate, years - 1) * (1 + rate);std::cout << "Projected balance after " << years << " years: " << future_balance << std::endl;return future_balance;}
};// 演示函数
void demonstrate_recursive_locks() {BankAccount account1(1000);BankAccount account2(500);std::cout << "=== Initial Balances ===" << std::endl;std::cout << "Account 1: " << account1.get_balance() << std::endl;std::cout << "Account 2: " << account2.get_balance() << std::endl;std::cout << "\n=== Demonstrating Transfer (Nested Locking) ===" << std::endl;account1.transfer(account2, 200);std::cout << "\n=== Balances After Transfer ===" << std::endl;std::cout << "Account 1: " << account1.get_balance() << std::endl;std::cout << "Account 2: " << account2.get_balance() << std::endl;std::cout << "\n=== Demonstrating Compound Interest Calculation (Recursive Function) ===" << std::endl;double projected = account1.calculate_compound_interest(0.05, 3);std::cout << "Projected balance after 3 years: " << projected << std::endl;
}// 多线程演示
void concurrent_access_demo() {BankAccount account(1000);// 创建多个线程同时访问账户std::thread t1([&account]() {account.deposit(100);});std::thread t2([&account]() {account.withdraw(200);});std::thread t3([&account]() {BankAccount other_account(500);account.transfer(other_account, 150);});t1.join();t2.join();t3.join();std::cout << "Final balance: " << account.get_balance() << std::endl;
}int main() {std::cout << "=== Recursive Lock Demonstration ===" << std::endl;std::cout << "\n----- Single Thread Demonstration -----" << std::endl;demonstrate_recursive_locks();std::cout << "\n----- Multi-Thread Demonstration -----" << std::endl;concurrent_access_demo();return 0;
}

代码解析

1. 嵌套调用示例 - transfer 方法

bool transfer(BankAccount& to_account, double amount) {std::lock_guard<std::recursive_mutex> lock(mtx_); // 第一次获取锁// ...if (this->withdraw(amount)) { // withdraw内部会再次获取同一个锁to_account.deposit(amount); // deposit内部会获取to_account的锁// ...}// ...
}

transfer方法中,我们首先获取了账户的锁,然后调用了withdraw方法,而withdraw方法内部也会尝试获取同一个锁。如果没有使用递归锁,这会导致死锁。

2. 递归函数示例 - calculate_compound_interest 方法

double calculate_compound_interest(double rate, int years) const {std::lock_guard<std::recursive_mutex> lock(mtx_); // 第一次获取锁// ...// 递归调用 - 这里会再次获取同一个锁double future_balance = calculate_compound_interest(rate, years - 1) * (1 + rate);// ...
}

这是一个递归函数,每次递归调用都会尝试获取同一个锁。只有递归锁才能允许这种情况发生。

3. 多线程安全性

即使我们使用递归锁,它仍然提供了多线程安全性:

  • 不同线程之间仍然是互斥的
  • 只有同一个线程可以多次获取同一个递归锁
  • 锁只有在计数器归零时才会真正释放

输出示例

运行此程序可能会产生类似以下的输出:

=== Recursive Lock Demonstration ===----- Single Thread Demonstration -----
=== Initial Balances ===
Account 1: 1000
Account 2: 500=== Demonstrating Transfer (Nested Locking) ===
Attempting transfer of: 200
Withdrew: 200, New Balance: 800
Deposited: 200, New Balance: 700
Transfer successful!=== Balances After Transfer ===
Account 1: 800
Account 2: 700=== Demonstrating Compound Interest Calculation (Recursive Function) ===
Calculating interest for year 3
Calculating interest for year 2
Calculating interest for year 1
Projected balance after 1 years: 840
Projected balance after 2 years: 882
Projected balance after 3 years: 926.1
Projected balance after 3 years: 926.1----- Multi-Thread Demonstration -----
Deposited: 100, New Balance: 1100
Withdrew: 200, New Balance: 900
Attempting transfer of: 150
Withdrew: 150, New Balance: 750
Deposited: 150, New Balance: 650
Transfer successful!
Final balance: 750

重要注意事项

  1. 谨慎使用递归锁:虽然这个示例展示了递归锁的用法,但在实际开发中,应该优先考虑重构代码以避免需要递归锁的情况。

  2. 锁的释放:必须确保解锁次数与加锁次数完全一致,否则会导致锁无法释放或其他线程无法获取锁。

  3. 性能考虑:递归锁通常比普通锁有更高的开销,因为需要维护计数器和管理所有者线程。

  4. 替代方案:对于transfer方法,更好的设计可能是创建不加锁的私有方法(如do_withdrawdo_deposit),然后由公共方法在持有锁的情况下调用这些方法。

这个示例展示了递归锁的两种常见使用场景,以及它们如何解决普通互斥量会导致死锁的问题。

为什么需要重复获取相同的锁

在这个示例中,有两种情况需要重复获取相同的递归锁:

  1. 嵌套调用(在transfer方法中):

    • transfer方法首先获取了账户的锁
    • 然后它调用了withdraw方法,而withdraw方法也需要获取同一个账户的锁
    • 如果没有递归锁,这种嵌套调用会导致死锁,因为同一线程尝试重复获取同一个普通互斥锁
    • 递归锁通过内部计数器跟踪锁的获取次数,允许同一线程多次获取同一个锁
  2. 递归函数(在calculate_compound_interest方法中):

    • 这是一个递归函数,每次递归调用都需要获取同一个锁来保护对余额的访问
    • 如果没有递归锁,第二次及以后的递归调用会导致死锁
    • 递归锁允许函数在递归调用过程中多次获取同一个锁,确保线程安全

需要注意的是,递归锁虽然解决了这些问题,但也增加了代码的复杂性和潜在的错误风险(如忘记释放锁)。在实际开发中,应该优先考虑重构代码以避免需要递归锁的情况。

重构代码逻辑避免递归锁使用

重构 BankAccount 类避免递归锁使用

下面我将重构代码,通过改进设计来避免使用递归锁。主要策略是将需要加锁的公共操作提取为不加锁的私有方法,然后在公共方法中统一加锁后调用这些私有方法。

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>// 银行账户类 - 重构后避免使用递归锁
class BankAccount {
private:double balance_; // 账户余额mutable std::mutex mtx_; // 使用普通互斥锁,不再需要递归锁// 不加锁的存款实现 - 由公共方法在持有锁的情况下调用void do_deposit(double amount) {balance_ += amount;std::cout << "Deposited: " << amount << ", New Balance: " << balance_ << std::endl;}// 不加锁的取款实现 - 由公共方法在持有锁的情况下调用bool do_withdraw(double amount) {if (balance_ >= amount) {balance_ -= amount;std::cout << "Withdrew: " << amount << ", New Balance: " << balance_ << std::endl;return true;}std::cout << "Failed to withdraw: " << amount << ", Insufficient balance: " << balance_ << std::endl;return false;}// 计算复利的辅助函数 - 不涉及共享数据访问,无需加锁double calculate_compound_interest_helper(double current_balance, double rate, int years) const {if (years <= 0) {return current_balance;}// 模拟复杂计算过程std::cout << "Calculating interest for year " << years << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));// 递归计算double future_balance = calculate_compound_interest_helper(current_balance, rate, years - 1) * (1 + rate);std::cout << "Projected balance after " << years << " years: " << future_balance << std::endl;return future_balance;}public:// 构造函数,初始化余额BankAccount(double initial_balance = 0) : balance_(initial_balance) {}// 存款方法 - 公共接口,内部加锁void deposit(double amount) {std::lock_guard<std::mutex> lock(mtx_);do_deposit(amount);}// 取款方法 - 公共接口,内部加锁bool withdraw(double amount) {std::lock_guard<std::mutex> lock(mtx_);return do_withdraw(amount);}// 查询余额方法 - 公共接口,内部加锁double get_balance() const {std::lock_guard<std::mutex> lock(mtx_);return balance_;}// 转账方法 - 重构后避免嵌套锁bool transfer(BankAccount& to_account, double amount) {// 使用std::lock同时锁定两个账户的互斥量,避免死锁std::unique_lock<std::mutex> lock1(mtx_, std::defer_lock);std::unique_lock<std::mutex> lock2(to_account.mtx_, std::defer_lock);std::lock(lock1, lock2); // 原子性地锁定两个互斥量std::cout << "Attempting transfer of: " << amount << std::endl;// 直接操作余额,而不是调用withdraw和deposit方法if (balance_ >= amount) {balance_ -= amount;to_account.balance_ += amount;std::cout << "Withdrew: " << amount << " from source, New Balance: " << balance_ << std::endl;std::cout << "Deposited: " << amount << " to target, New Balance: " << to_account.balance_ << std::endl;std::cout << "Transfer successful!" << std::endl;return true;}std::cout << "Transfer failed! Insufficient balance: " << balance_ << std::endl;return false;}// 计算复利方法 - 重构后避免递归锁double calculate_compound_interest(double rate, int years) const {double current_balance;{// 首先获取当前余额的快照std::lock_guard<std::mutex> lock(mtx_);current_balance = balance_;}// 基于快照进行计算,不需要持有锁// 这样可以避免在递归调用中重复获取锁std::cout << "Calculating compound interest based on current balance: " << current_balance << std::endl;return calculate_compound_interest_helper(current_balance, rate, years);}
};// 演示函数 - 展示重构后的代码在单线程环境下的使用
void demonstrate_refactored_locks() {BankAccount account1(1000);BankAccount account2(500);std::cout << "=== Initial Balances ===" << std::endl;std::cout << "Account 1: " << account1.get_balance() << std::endl;std::cout << "Account 2: " << account2.get_balance() << std::endl;std::cout << "\n=== Demonstrating Transfer ===" << std::endl;account1.transfer(account2, 200);std::cout << "\n=== Balances After Transfer ===" << std::endl;std::cout << "Account 1: " << account1.get_balance() << std::endl;std::cout << "Account 2: " << account2.get_balance() << std::endl;std::cout << "\n=== Demonstrating Compound Interest Calculation ===" << std::endl;double projected = account1.calculate_compound_interest(0.05, 3);std::cout << "Projected balance after 3 years: " << projected << std::endl;
}// 多线程演示 - 展示重构后的代码在多线程环境下的使用
void concurrent_access_demo() {BankAccount account(1000);// 创建多个线程同时访问账户std::thread t1([&account]() {account.deposit(100);});std::thread t2([&account]() {account.withdraw(200);});std::thread t3([&account]() {BankAccount other_account(500);account.transfer(other_account, 150);});t1.join();t2.join();t3.join();std::cout << "Final balance: " << account.get_balance() << std::endl;
}int main() {std::cout << "=== Refactored BankAccount (No Recursive Locks) ===" << std::endl;std::cout << "\n----- Single Thread Demonstration -----" << std::endl;demonstrate_refactored_locks();std::cout << "\n----- Multi-Thread Demonstration -----" << std::endl;concurrent_access_demo();return 0;
}

重构策略说明

1. 提取不加锁的私有方法

创建了 do_deposit()do_withdraw() 私有方法,这些方法执行实际的操作但不加锁。公共方法 deposit()withdraw() 现在只需要获取一次锁,然后调用这些私有方法。

解决的问题:避免了在 transfer() 方法中嵌套调用加锁的公共方法。

2. 重构 transfer() 方法

不再调用 withdraw()deposit() 方法,而是直接操作余额:

  • 使用 std::lock() 同时锁定两个账户的互斥量,避免死锁
  • 直接修改两个账户的余额,而不是通过方法调用
  • 使用 std::unique_lockstd::defer_lock 来延迟加锁,以便能够使用 std::lock()

解决的问题:避免了嵌套锁和递归锁的需要。

3. 重构 calculate_compound_interest() 方法

将计算逻辑分为两部分:

  1. 获取当前余额的快照(需要加锁)
  2. 基于快照进行递归计算(不需要加锁)

创建了辅助函数 calculate_compound_interest_helper() 来处理递归计算,这个函数不访问共享数据,因此不需要加锁。

解决的问题:避免了在递归函数中重复获取锁。

优点

  1. 更简单的锁机制:使用普通 std::mutex 代替 std::recursive_mutex,减少了复杂性和潜在错误。
  2. 更清晰的代码结构:将业务逻辑与锁机制分离,提高了代码的可读性和可维护性。
  3. 更好的性能:减少了锁的获取和释放次数,特别是在递归计算中。
  4. 减少死锁风险:使用 std::lock() 同时锁定多个互斥量,避免了死锁的可能性。

注意事项

  1. 快照的一致性:在 calculate_compound_interest() 中,我们基于余额的快照进行计算,这意味着计算结果可能不反映实时的账户状态。如果需要实时一致性,可能需要不同的方法。

  2. 异常安全:确保在异常情况下锁能够正确释放,使用 RAII 包装器(如 std::lock_guardstd::unique_lock)可以保证这一点。

  3. 性能考虑:对于简单的操作,提取私有方法可能增加函数调用开销,但在大多数情况下,这种开销可以忽略不计,特别是与锁操作的开销相比。

这种重构展示了如何通过改进设计来避免使用递归锁,从而使代码更加健壮和易于维护。

http://www.xdnf.cn/news/20508.html

相关文章:

  • Kimi K2-0905 256K 上下文 API 状态管理优化教程
  • 2.虚拟内存:分页、分段、页面置换算法
  • 分享一个基于Python+大数据的房地产一手房成交数据关联分析与可视化系统,基于机器学习的深圳房产价格走势分析与预测系统
  • Embedding上限在哪里?- On the Theoretical Limitations of Embedding-Based Retrieval
  • AI产品经理面试宝典第86天:提示词设计核心原则与面试应答策略
  • 《sklearn机器学习——聚类性能指标》Calinski-Harabaz 指数
  • Wisdom SSH 是一款搭载强大 AI 助手的工具,能显著简化服务器配置管理流程。
  • SSH服务远程安全登录
  • Linux系统shell脚本(四)
  • CodeSandbox Desktop:零配置项目启动工具,实现项目环境隔离与Github无缝同步
  • AI大模型应用研发工程师面试知识准备目录
  • 苍穹外卖优化-续
  • Java包装类型
  • Git 长命令变短:一键设置别名
  • Linux以太网模块
  • 【嵌入式】【科普】AUTOSAR学习路径
  • 《无畏契约》游戏报错“缺少DirectX”?5种解决方案(附DirectX修复工具)
  • 基于单片机智能行李箱设计
  • 云手机运行流畅,秒开不卡顿
  • 无拥塞网络的辩证
  • 24.线程概念和控制(一)
  • 贪心算法应用:数字孪生同步问题详解
  • B.50.10.10-微服务与电商应用
  • 关于退耦电容
  • 【LeetCode热题100道笔记】将有序数组转换为二叉搜索树
  • 3分钟快速入门WebSocket
  • Scikit-learn Python机器学习 - 特征降维 压缩数据 - 特征提取 - 主成分分析 (PCA)
  • dify+Qwen2.5-vl+deepseek打造属于自己的作业帮
  • 第27节:3D数据可视化与大规模地形渲染
  • 如何下载小红书视频