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。
- 当线程第一次成功获取递归锁时,锁被标记为已锁定,并记录所有者线程ID,计数器设为 1。
- 当同一个线程再次请求锁时,计数器简单地递增(例如,变为 2),线程继续执行。
- 每次调用
unlock()
时,计数器递减。 - 只有当计数器减回到 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_mutex
和 std::recursive_timed_mutex
C++ 标准库提供了两种递归锁:
-
std::recursive_mutex
:- 基本的递归互斥量。
- 成员函数和
std::mutex
一样:lock()
,try_lock()
,unlock()
。
-
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),应谨慎使用。
- 逻辑复杂性:需要递归锁的设计往往意味着你的代码结构可能不够清晰。函数之间的调用关系变得复杂,锁的持有时间也变长了(从第一次加锁到最后一次解锁),这可能会降低性能。
- 容易出错:你必须确保 解锁的次数完全等于加锁的次数。如果少解锁一次,锁就永远不会被释放;如果多解锁一次,会导致未定义行为。
- 掩盖设计问题:有时,使用递归锁只是掩盖了更深层次的设计缺陷。更好的方法可能是:
- 重构代码:将需要加锁的公共部分提取成一个新的私有函数,这个函数由原函数调用,但自身不加锁(需要调用者保证已持有锁)。这样公共部分只需要加一次锁。
- 使用不可重入的设计:重新思考函数职责,避免在已持有锁的情况下调用另一个也需要相同锁的函数。
总结
特性 | 普通锁 (e.g., std::mutex ) | 递归锁 (e.g., std::recursive_mutex ) |
---|---|---|
同一线程重复加锁 | 导致未定义行为/死锁 | 允许,内部计数器递增 |
性能 | 通常更高 | 稍低(需要维护计数器和管理所有者线程) |
使用场景 | 绝大多数常规的临界区保护 | 递归函数、复杂回调、难以重构的遗留代码 |
推荐度 | 首选 | 慎用,作为最后手段 |
核心建议: 首先优先考虑代码设计,尽量避免需要递归锁的场景。如果确实无法避免(例如第三方库的回调要求),再使用 std::recursive_mutex
,并且要非常小心地管理锁的获取和释放次数。
递归锁(Recursive Lock)完整示例
下面我将通过一个模拟"银行账户"的场景来演示递归锁的使用。这个例子中,我们会看到两种需要递归锁的情况:递归函数调用和嵌套调用。
场景描述
假设我们有一个BankAccount
类,它包含:
- 基本操作:存款(
deposit
)、取款(withdraw
)、查询余额(get_balance
) - 复杂操作:转账(
transfer
),它内部会调用取款和存款 - 利息计算(
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
重要注意事项
-
谨慎使用递归锁:虽然这个示例展示了递归锁的用法,但在实际开发中,应该优先考虑重构代码以避免需要递归锁的情况。
-
锁的释放:必须确保解锁次数与加锁次数完全一致,否则会导致锁无法释放或其他线程无法获取锁。
-
性能考虑:递归锁通常比普通锁有更高的开销,因为需要维护计数器和管理所有者线程。
-
替代方案:对于
transfer
方法,更好的设计可能是创建不加锁的私有方法(如do_withdraw
和do_deposit
),然后由公共方法在持有锁的情况下调用这些方法。
这个示例展示了递归锁的两种常见使用场景,以及它们如何解决普通互斥量会导致死锁的问题。
为什么需要重复获取相同的锁
在这个示例中,有两种情况需要重复获取相同的递归锁:
-
嵌套调用(在
transfer
方法中):transfer
方法首先获取了账户的锁- 然后它调用了
withdraw
方法,而withdraw
方法也需要获取同一个账户的锁 - 如果没有递归锁,这种嵌套调用会导致死锁,因为同一线程尝试重复获取同一个普通互斥锁
- 递归锁通过内部计数器跟踪锁的获取次数,允许同一线程多次获取同一个锁
-
递归函数(在
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_lock
和std::defer_lock
来延迟加锁,以便能够使用std::lock()
解决的问题:避免了嵌套锁和递归锁的需要。
3. 重构 calculate_compound_interest()
方法
将计算逻辑分为两部分:
- 获取当前余额的快照(需要加锁)
- 基于快照进行递归计算(不需要加锁)
创建了辅助函数 calculate_compound_interest_helper()
来处理递归计算,这个函数不访问共享数据,因此不需要加锁。
解决的问题:避免了在递归函数中重复获取锁。
优点
- 更简单的锁机制:使用普通
std::mutex
代替std::recursive_mutex
,减少了复杂性和潜在错误。 - 更清晰的代码结构:将业务逻辑与锁机制分离,提高了代码的可读性和可维护性。
- 更好的性能:减少了锁的获取和释放次数,特别是在递归计算中。
- 减少死锁风险:使用
std::lock()
同时锁定多个互斥量,避免了死锁的可能性。
注意事项
-
快照的一致性:在
calculate_compound_interest()
中,我们基于余额的快照进行计算,这意味着计算结果可能不反映实时的账户状态。如果需要实时一致性,可能需要不同的方法。 -
异常安全:确保在异常情况下锁能够正确释放,使用 RAII 包装器(如
std::lock_guard
和std::unique_lock
)可以保证这一点。 -
性能考虑:对于简单的操作,提取私有方法可能增加函数调用开销,但在大多数情况下,这种开销可以忽略不计,特别是与锁操作的开销相比。
这种重构展示了如何通过改进设计来避免使用递归锁,从而使代码更加健壮和易于维护。