核心目标:深入理解 C++ 互斥锁体系,掌握正确保护共享数据的方法,熟练运用条件变量实现生产者-消费者模式,并有效避免死锁问题。
前置知识 :Part 1 中的
std::thread基础(线程创建、join 操作、参数传递等)。
2.1 数据竞争(Data Race)与未定义行为
2.1.1 数据竞争的本质
当多个线程同时访问同一内存位置,且至少有一个线程执行写操作,而缺乏适当的同步机制时,就会发生数据竞争(Data Race) 。根据 C++ 标准,数据竞争属于未定义行为,程序的行为将无法预测。
cpp
#include <thread>
#include <iostream>
int counter = 0; // 共享变量,无保护
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // ❌ Data Race! 两个线程同时读写
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter = " << counter << "\n";
// 预期: 200000
// 实际: 通常是 120000~180000 之间的随机值!
}
2.1.2 汇编视角看 counter++
; counter++ 不是原子操作, 而是三步:
;
; mov eax, [counter] ; ① 从内存加载到寄存器
; inc eax ; ② 寄存器中递增
; mov [counter], eax ; ③ 写回内存
;
; 两个线程交错执行:
; Thread A: ① load (counter=0) → ② inc (eax=1) → ③ store (counter=1)
; Thread B: ① load (counter=0) → ② inc (eax=1) → ③ store (counter=1)
; 结果: 两次递增, counter 只增加了 1!
Thread B
Thread A
交错
覆盖
load: 0
inc: 1
store: 1
load: 0
inc: 1
store: 1
结果: counter = 1
(期望 2)
检测工具:用 Thread Sanitizer 一秒发现:
bashg++ -std=c++17 -pthread -fsanitize=thread -g race.cpp ./a.out # TSAN: WARNING: ThreadSanitizer: data race on counter
2.2 std::mutex------保护共享数据
2.2.1 基本用法
cpp
#include <mutex>
int counter = 0;
std::mutex mtx; // 互斥量
void increment_safe() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
++counter; // 临界区
mtx.unlock();
}
}
// 结果: counter = 200000 ✅ 正确
2.2.2 为什么不直接使用 lock()/unlock()
cpp
void dangerous() {
mtx.lock();
// ⚠️ 如果这里有异常或 return ...
throw std::runtime_error("oops");
// unlock() 永远不会执行 → 死锁!
mtx.unlock();
}
2.2.3 std::lock_guard------RAII 自动管理锁
cpp
void safe_with_guard() {
std::lock_guard<std::mutex> lock(mtx); // 构造时 lock
++counter;
// ... 复杂逻辑, 可能抛异常 ...
// 无论怎样退出作用域, 析构函数自动 unlock
}
进入作用域
lock_guard 构造
→ mtx.lock()
✓ 临界区代码
(任何异常都安全)
离开作用域
抛异常
lock_guard 析构
→ mtx.unlock()
2.2.4 C++17 CTAD 简化写法
cpp
// C++14 写法
std::lock_guard<std::mutex> lock(mtx);
// C++17 CTAD (类模板参数推导) ------ 自动推导类型
std::lock_guard lock(mtx); // ✅ 等效
std::scoped_lock lock(mtx); // ✅ C++17 推荐 (后面详解)
2.3 std::unique_lock------灵活性优于 lock_guard
2.3.1 与 lock_guard 的对比
| 特性 | std::lock_guard |
std::unique_lock |
|---|---|---|
| RAII 自动管理 | ✅ | ✅ |
| 手动 unlock | ❌ | ✅ |
| 延迟加锁 | ❌ | ✅ (defer_lock) |
| 尝试加锁 | ❌ | ✅ (try_to_lock) |
| 可移动 | ❌ | ✅ (moveable) |
| 适合 condition_variable | ❌ | ✅ 必须 |
| 开销 | 最小 | 稍大(多了状态标志) |
2.3.2 延迟加锁 std::defer_lock
cpp
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 此时 mtx 未被锁定
// 稍后手动加锁
lock.lock();
// ... 临界区 ...
lock.unlock();
// 再次加锁也可以
lock.lock();
// ... 另一个临界区 ...
2.3.3 尝试加锁 std::try_to_lock
cpp
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock) { // 或 lock.owns_lock()
// 成功获得锁
++counter;
} else {
// 锁被占用, 做点别的事
do_other_work();
}
2.3.4 接管已有锁 std::adopt_lock
cpp
mtx.lock(); // 手动加锁
// ... 一些前置操作 ...
// 将已锁的 mutex 交给 unique_lock 管理
std::unique_lock<std::mutex> lock(mtx, std::adopt_lock);
// lock 对象接管所有权, 析构时会自动 unlock
2.3.5 应用场景选择
cpp
// 场景 1: 简单临界区 → lock_guard (够用且零开销)
{
std::lock_guard lock(mtx);
shared_data = new_value;
}
// 场景 2: 条件变量 → unique_lock (必须)
std::unique_lock lock(mtx);
cv.wait(lock, []{ return ready; });
// 场景 3: 需要中途解锁 → unique_lock
std::unique_lock lock(mtx);
while (!done) {
lock.unlock(); // 释放锁
do_io_work(); // IO 不应持锁
lock.lock(); // 重新获取
}
// 场景 4: 需要移动 → unique_lock
std::unique_lock lock1(mtx);
std::unique_lock lock2 = std::move(lock1); // 转移所有权
2.4 std::scoped_lock(C++17)------多锁 RAII 利器
2.4.1 同时锁定多个 mutex
cpp
std::mutex mtx1, mtx2;
// C++14 写法: 容易出错
void transfer_cpp14(int amount) {
std::lock(mtx1, mtx2); // 同时锁定两个
std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock);
// ... 临界区 ...
}
// C++17 写法: 一行搞定
void transfer_cpp17(int amount) {
std::scoped_lock lock(mtx1, mtx2); // ✅ 自动多锁 + 防死锁
// ... 临界区 ...
}
std::scoped_lock 等价于 std::lock() + N 个 lock_guard,但更简洁、更不易出错。
2.4.2 scoped_lock 防死锁原理
cpp
// std::scoped_lock 内部使用 std::lock() 算法:
// 如果无法同时获得所有锁 → 释放已获得的 → 重试
// 避免了"th1 持有 A 等 B,th2 持有 B 等 A"的经典死锁
2.5 死锁与避免策略
2.5.1 死锁经典场景
cpp
std::mutex mtx_a, mtx_b;
// Thread 1: 先锁 A, 再锁 B
void thread1() {
std::lock_guard lock_a(mtx_a);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard lock_b(mtx_b); // 等待 B
// ... 永远不会执行到 ...
}
// Thread 2: 先锁 B, 再锁 A
void thread2() {
std::lock_guard lock_b(mtx_b);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard lock_a(mtx_a); // 等待 A
// ... 永远不会执行到 ...
}
Thread 2 Mutex B Mutex A Thread 1 Thread 2 Mutex B Mutex A Thread 1 🔒 死锁! 互相等待 lock A ✅ lock B ✅ 等待 lock B... 等待 lock A...
2.5.2 避免死锁的四种策略
策略 1:固定上锁顺序
cpp
// ✅ 所有线程按相同顺序锁定
void transfer(Account& from, Account& to, int amount) {
// 按地址排序, 确保锁顺序一致
if (&from < &to) {
std::scoped_lock lock(from.mtx, to.mtx);
// ...
} else {
std::scoped_lock lock(to.mtx, from.mtx);
// ...
}
}
策略 2:使用 std::lock() 或 std::scoped_lock
cpp
// ✅ 让标准库处理顺序
std::scoped_lock lock(mtx_a, mtx_b, mtx_c); // 自动防死锁
策略 3:使用层级锁(Hierarchical Lock)
cpp
// 自定义: 只能从高层级向低层级加锁
class HierarchicalMutex {
std::mutex mtx_;
int level_;
static thread_local int this_thread_level_;
public:
explicit HierarchicalMutex(int level) : level_(level) {}
void lock() {
if (this_thread_level_ <= level_) {
throw std::logic_error("lock order violation!");
}
mtx_.lock();
this_thread_level_ = level_;
}
void unlock() {
this_thread_level_ = level_ + 1;
mtx_.unlock();
}
};
策略 4:避免嵌套锁
cpp
// ❌ 持有锁时调用外部函数
void bad() {
std::lock_guard lock(mtx);
external_function(); // 不知道它会不会加锁!
}
// ✅ 重构: 锁仅保护数据, 不在持锁时调用外部代码
void good() {
int local;
{
std::lock_guard lock(mtx);
local = shared_data;
} // 释放锁
external_function(local); // 安全
}
2.6 其他互斥量类型
2.6.1 std::recursive_mutex
cpp
class RecursiveDemo {
std::recursive_mutex mtx_;
int data_ = 0;
public:
void outer() {
std::lock_guard lock(mtx_);
++data_;
inner(); // ✅ 同一线程再次加锁, 不阻塞
}
void inner() {
std::lock_guard lock(mtx_); // 递归加锁
++data_;
}
};
// ⚠️ 大多数情况不需要 recursive_mutex
// 如果设计中出现需要递归加锁, 往往说明锁的边界划分有问题
2.6.2 std::timed_mutex
cpp
std::timed_mutex tmtx;
void try_acquire_with_timeout() {
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
if (tmtx.try_lock_until(deadline)) { // 等待最多 1 秒
std::lock_guard<std::timed_mutex> lock(tmtx, std::adopt_lock);
// ... 临界区 ...
} else {
std::cout << "未能获得锁, 超时\n";
}
}
// 也可以用 try_lock_for
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
// ...
}
2.7 std::once_flag 与 std::call_once
2.7.1 线程安全的单次初始化
cpp
std::once_flag init_flag;
ExpensiveResource* resource = nullptr;
void initialize_resource() {
resource = new ExpensiveResource(); // 只会被调用一次
}
void use_resource() {
std::call_once(init_flag, initialize_resource);
resource->do_work(); // 安全: 保证初始化已完成
}
2.7.2 比 DCLP 更安全
cpp
// ❌ 双重检查锁定 (DCLP) ------ 在 C++11 之前是 broken 的
ExpensiveResource* dclp_broken() {
if (resource == nullptr) { // ① 第一次检查
std::lock_guard lock(mtx);
if (resource == nullptr) { // ② 第二次检查
resource = new ExpensiveResource();
// ⚠️ C++03: new 的三步可能重排 → 其他线程看到"半成品"
}
}
return resource;
}
// ✅ C++11+: 用 call_once 或 函数内 static
ExpensiveResource& safe_singleton() {
static ExpensiveResource instance; // C++11 保证线程安全
return instance;
}
2.8 std::condition_variable------等待与唤醒
2.8.1 为什么需要条件变量
cpp
// ❌ 忙等待 (Busy Wait): 浪费 CPU
while (!ready) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 还是忙
}
// ✅ 条件变量: 让线程休眠, 直到条件满足
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waiter() {
std::unique_lock lock(mtx);
cv.wait(lock, []{ return ready; }); // 自动 unlock + 休眠 + 醒来 + lock
// 此时已持有锁, ready == true
std::cout << "Processing...\n";
}
void signaler() {
{
std::lock_guard lock(mtx);
ready = true;
}
cv.notify_one(); // 唤醒一个等待线程
}
2.8.2 wait() 的执行流程
是
否
wait(lock, predicate)
predicate()
== true?
继续执行
(已持有锁)
① unlock
② 线程休眠
③ 被 notify 唤醒
④ 重新 lock
2.8.3 为什么必须循环检查(虚假唤醒)
cpp
// ❌ 错误: 只用 if 检查
cv.wait(lock); // 不带 predicate
if (ready) { // 虚假唤醒后不会重新检查!
// ...
}
// ✅ 正确写法 1: Lambda predicate
cv.wait(lock, []{ return ready; });
// ✅ 正确写法 2: while 循环 (等效)
while (!ready) {
cv.wait(lock);
}
虚假唤醒(Spurious Wakeup):操作系统可能没有原因地唤醒线程。用 predicate 或 while 循环自动防御。
2.8.4 notify_one vs notify_all
cpp
// notify_one: 唤醒一个等待线程
cv.notify_one(); // 适合: 一次生产, 一个消费者处理
// notify_all: 唤醒所有等待线程
cv.notify_all(); // 适合: 状态变更影响所有等待者
2.9 生产者-消费者模式
2.9.1 经典实现:有界缓冲区
cpp
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>
template <typename T>
class BoundedQueue {
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_not_full_;
std::condition_variable cv_not_empty_;
size_t capacity_;
public:
explicit BoundedQueue(size_t capacity) : capacity_(capacity) {}
// 生产者: 放入元素 (队列满则阻塞)
void push(T item) {
std::unique_lock lock(mtx_);
cv_not_full_.wait(lock, [this] {
return queue_.size() < capacity_;
});
queue_.push(std::move(item));
cv_not_empty_.notify_one();
}
// 消费者: 取出元素 (队列空则阻塞)
T pop() {
std::unique_lock lock(mtx_);
cv_not_empty_.wait(lock, [this] {
return !queue_.empty();
});
T item = std::move(queue_.front());
queue_.pop();
cv_not_full_.notify_one();
return item;
}
// 非阻塞尝试
std::optional<T> try_pop() {
std::lock_guard lock(mtx_);
if (queue_.empty()) return std::nullopt;
T item = std::move(queue_.front());
queue_.pop();
cv_not_full_.notify_one();
return item;
}
};
2.9.2 交互时序
Consumer BoundedQueue Producer Consumer BoundedQueue Producer 队列满, Producer 等待 push(item1) notify 唤醒 push(item2) push(item3) pop() → item1 notify 唤醒 Producer push(item4)
2.9.3 完整使用示例
cpp
int main() {
BoundedQueue<int> queue(5);
// 生产者线程
std::thread producer([&queue] {
for (int i = 0; i < 20; ++i) {
queue.push(i);
std::cout << "Produced: " << i << "\n";
}
});
// 消费者线程
std::thread consumer([&queue] {
for (int i = 0; i < 20; ++i) {
int item = queue.pop();
std::cout << "Consumed: " << item << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
producer.join();
consumer.join();
}
2.10 锁的粒度与性能
2.10.1 锁的边界划定
cpp
// ❌ 锁太粗: 整个函数持锁
void too_coarse() {
std::lock_guard lock(mtx);
auto data = expensive_query_from_db(); // 耗时的 IO, 不应持锁!
process(data); // 耗时计算
update_shared_state(data); // 只有这里需要锁
}
// ✅ 细化粒度: 只在需要的时候持锁
void fine_grained() {
auto data = expensive_query_from_db(); // 无锁
auto result = process(data); // 无锁
std::lock_guard lock(mtx); // 最小临界区
shared_state.update(result);
}
2.10.2 性能数据参考
| 操作 | 耗时(相对) | 说明 |
|---|---|---|
| 无竞争 mutex::lock | 1x (~20ns) | 非常快 |
| 有轻竞争 mutex::lock | 10-100x | 自旋等待 |
| 有严重竞争 mutex::lock | 1000x+ | 系统调用 + 上下文切换 |
| condition_variable::wait | ~2000x | 系统调用 + 调度 |
| 系统调用 (read/write) | ~500x | IO 开销 |
| 上下文切换 | ~5000x | 最贵 |
设计原则:
- 持锁时不调用外部代码(你不知道它会做什么)
- 持锁时不做 IO(IO 耗时远大于锁操作)
- 持锁时不获取另一个锁(除非使用
std::scoped_lock)- 临界区代码越短越好
2.11 小结
| 知识点 | 掌握程度 | 核心要点 |
|---|---|---|
| Data Race 的危害 | 掌握 | 用 TSAN(-fsanitize=thread) 自动检测 |
std::lock_guard |
熟练 | 最简单安全的 RAII 锁 |
std::unique_lock |
掌握 | 灵活性, 条件变量必须搭配 |
std::scoped_lock (C++17) |
掌握 | 多锁场景首选 |
| 死锁避免 | 掌握 | 固定顺序 / std::scoped_lock / 避免嵌套锁 |
std::recursive_mutex |
理解 | 谨慎使用, 往往设计有改进空间 |
std::timed_mutex |
理解 | 超时避免永久阻塞 |
std::call_once |
掌握 | 优于 DCLP, 线程安全初始化 |
condition_variable |
掌握 | 必须用 predicate 防虚假唤醒 |
| 生产者-消费者 | 掌握 | 双条件变量模式 |
| 锁粒度优化 | 理解 | 最小化临界区, 不持锁做 IO |
下期预告
[Part 3:原子操作与内存模型] 将深入无锁编程:
std::atomic<T>------ 真正的原子操作compare_exchange_weak/strong------ CAS 循环memory_order六种内存序详解- 无锁栈(Lock-Free Stack)实现
- 原子操作 vs 互斥锁的性能对比
推荐工具
-fsanitize=thread -g------ 一秒发现 Data Racevalgrind --tool=helgrind------ 检测锁顺序问题- GDB
thread apply all bt------ 定位死锁