mutex 与 lock_guard
定义与作用
互斥锁(mutex)提供线程间互斥访问,lock_guard 利用 RAII 自动管理加锁/解锁。
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁
// 临界区
} // 析构时自动解锁
核心原理
互斥锁类型层次
死锁四条件
完整示例
示例一:飞翔科技线程安全计数器
场景说明:小崔实现一个线程安全的请求计数器供孔蓝的 API 网关使用。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>
#include <iomanip>
class RequestCounter {
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx_);
++count_;
}
uint64_t get() const {
std::lock_guard<std::mutex> lock(mtx_);
return count_;
}
// 重置并返回旧值(需要原子性)
uint64_t reset() {
std::lock_guard<std::mutex> lock(mtx_);
auto old = count_;
count_ = 0;
return old;
}
private:
mutable std::mutex mtx_; // mutable 允许在 const 方法中加锁
uint64_t count_ = 0;
};
int main() {
std::cout << "=== 飞翔科技 API 请求计数器 ===\n\n";
RequestCounter counter;
auto worker = [&counter](int id, int iterations) {
for (int i = 0; i < iterations; ++i) {
counter.increment();
// 模拟请求处理
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
};
const int kThreads = 8;
const int kIterPerThread = 10000;
std::vector<std::thread> threads;
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < kThreads; ++i)
threads.emplace_back(worker, i, kIterPerThread);
for (auto& t : threads) t.join();
auto end = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
uint64_t total = counter.get();
uint64_t expected = kThreads * kIterPerThread;
std::cout << "线程数: " << kThreads << "\n";
std::cout << "每线程请求: " << kIterPerThread << "\n";
std::cout << "期望总数: " << expected << "\n";
std::cout << "实际计数: " << total << "\n";
std::cout << "数据一致性: " << (total == expected ? "✓ 正确" : "✗ 错误") << "\n";
std::cout << "总耗时: " << ms << "ms\n";
std::cout << "吞吐量: " << (total * 1000.0 / ms) << " req/s\n";
counter.reset();
std::cout << "重置后计数: " << counter.get() << "\n";
}
预期输出:
=== 飞翔科技 API 请求计数器 ===
线程数: 8
每线程请求: 10000
期望总数: 80000
实际计数: 80000
数据一致性: ✓ 正确
总耗时: XXXms
吞吐量: XXXXX req/s
重置后计数: 0
逐段分析:
lock_guard在构造时mtx_.lock(),析构时mtx_.unlock()mutable修饰mtx_使const方法也可加锁- 每个
increment()调用都是线程安全的 - 80000 次操作零竞争错误
示例二:std::lock 同时加多锁防死锁
场景说明:白歌实现飞翔科技的双向转账,展示 std::lock 同时锁定两个账户。
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <vector>
class Account {
public:
Account(std::string name, double balance)
: name_(std::move(name)), balance_(balance) {}
double balance() const {
std::lock_guard<std::mutex> lock(mtx_);
return balance_;
}
// 线程安全转账(使用 std::lock 同时锁定两个账户)
static void transfer(Account& from, Account& to, double amount) {
if (&from == &to) return; // 同一账户无需加锁
// std::lock 同时锁定两个 mutex,避免死锁
std::lock(from.mtx_, to.mtx_);
// 已加锁,使用 adopt_lock 告诉 lock_guard 不要再加锁
std::lock_guard<std::mutex> lockFrom(from.mtx_, std::adopt_lock);
std::lock_guard<std::mutex> lockTo(to.mtx_, std::adopt_lock);
if (from.balance_ >= amount) {
from.balance_ -= amount;
to.balance_ += amount;
std::cout << " 转账: " << from.name_ << " → " << to.name_
<< " ¥" << amount << "\n";
} else {
std::cout << " 转账失败: " << from.name_ << " 余额不足\n";
}
}
private:
std::string name_;
double balance_;
mutable std::mutex mtx_;
};
int main() {
std::cout << "=== 飞翔科技内部转账系统 ===\n\n";
Account a("大翔-工资卡", 100000.00);
Account b("白歌-储蓄卡", 50000.00);
Account c("小崔-零花钱", 10000.00);
std::cout << "初始余额:\n";
std::cout << " " << "大翔: ¥" << a.balance() << "\n";
std::cout << " " << "白歌: ¥" << b.balance() << "\n";
std::cout << " " << "小崔: ¥" << c.balance() << "\n\n";
// 模拟并发转账
std::vector<std::thread> threads;
// 可能产生死锁的并发转账
threads.emplace_back([&]() {
for (int i = 0; i < 5; ++i) {
Account::transfer(a, b, 1000.0); // a → b
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
threads.emplace_back([&]() {
for (int i = 0; i < 5; ++i) {
Account::transfer(b, a, 500.0); // b → a(反向!)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
threads.emplace_back([&]() {
for (int i = 0; i < 3; ++i) {
Account::transfer(c, a, 2000.0); // c → a
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
});
for (auto& t : threads) t.join();
std::cout << "\n最终余额:\n";
std::cout << " " << "大翔: ¥" << a.balance() << "\n";
std::cout << " " << "白歌: ¥" << b.balance() << "\n";
std::cout << " " << "小崔: ¥" << c.balance() << "\n";
double total = a.balance() + b.balance() + c.balance();
std::cout << "总额不变性: ¥" << total << " (初始: ¥160000.00) "
<< (total == 160000.00 ? "✓" : "✗") << "\n";
}
预期输出:
=== 飞翔科技内部转账系统 ===
初始余额:
大翔: ¥100000
白歌: ¥50000
小崔: ¥10000
转账: 大翔-工资卡 → 白歌-储蓄卡 ¥1000
转账: 白歌-储蓄卡 → 大翔-工资卡 ¥500
...
最终余额:
大翔: ¥XXX
白歌: ¥XXX
小崔: ¥XXX
总额不变性: ¥160000.00 (初始: ¥160000.00) ✓
逐段分析:
std::lock(m1, m2)使用死锁避免算法同时锁定两个 mutexstd::adopt_lock告诉lock_guard互斥锁已锁定,无需再加锁- 两个线程以相反顺序转账(a→b 和 b→a),若用两行独立
lock()会死锁 - 总额不变性验证:转账不会创造或毁灭资金
易错场景与面试考点
易错场景
1. 忘记 unlock 或 lock_guard 作用域错误
void bad() {
mtx.lock();
if (error) return; // ❌ 提前返回未解锁
mtx.unlock();
}
// ✅ 使用 lock_guard,自动管理
2. 双锁死锁
// ❌ 如果两个线程同时分别锁住 a 和 b
thread1: a.lock(); b.lock(); // 持有 a,等待 b
thread2: b.lock(); a.lock(); // 持有 b,等待 a → 死锁!
// ✅ 使用 std::lock(a, b) 或统一加锁顺序
3. recursive_mutex 过度使用
// recursive_mutex 可能是设计缺陷的信号
// 通常意味着类的职责不清晰,应该重构
面试考点
| 考点 | 要点 |
|---|---|
| lock_guard | 最简单 RAII,不可手动解锁,不可转移 |
| unique_lock | 灵活 RAII,可延迟/提前/转移 |
| scoped_lock(C++17) | 可变参数,同时锁多个 mutex |
| 死锁四条件 | 互斥、持有等待、不可剥夺、循环等待 |
| std::lock | 避免死锁的同时加多锁 |
| std::adopt_lock | 标记已锁定,lock_guard 不再加锁 |
| std::defer_lock | 标记延后加锁,后续手动或 std::lock |
| std::try_to_lock | 尝试加锁,失败不阻塞 |
| recursive_mutex | 同一线程可多次 lock,需等量 unlock |