【学习笔记08】C++并发编程:条件变量与线程安全(下)
📝 说明:本文为C++学习笔记,AI辅助系统整理
📅 整理时间:2025年10月
🎯 课时:课时06 - 并发编程基础(下篇)
💡 核心内容:条件变量、线程安全类设计、死锁避免、原子操作
⭐ 难度等级:⭐⭐⭐⭐⭐

🎯 前言
这是课时06的下半部分,在掌握了基本的线程管理和互斥锁使用后,本文深入学习线程间通信、线程安全类设计、死锁问题以及高性能的原子操作。这些是C++后端开发中必须掌握的核心技能。
上篇回顾:
- std::thread线程基础(join/detach)
- std::mutex互斥锁与数据竞争
- std::lock_guard自动锁管理
- std::unique_lock灵活锁管理
本篇内容:
- std::condition_variable条件变量
- 线程安全类设计原则
- 死锁问题与避免策略
- std::atomic原子操作
📚 本文内容
🔔 核心知识点五:std::condition_variable条件变量
为什么需要条件变量?
目前学到的工具:
mutex:保护共享数据lock_guard/unique_lock:自动管理锁
但是缺少:线程之间如何通信和协作?
问题场景:生产者-消费者模型
错误的解决方案:忙等待
queue<int> dataQueue;
mutex mtx;
void consumer() {
for (int i = 0; i < 5; i++) {
while (true) {
lock_guard<mutex> lock(mtx);
if (!dataQueue.empty()) {
int data = dataQueue.front();
dataQueue.pop();
cout << "消费: " << data << "\n";
break;
}
// ❌ 问题:反复加锁解锁,浪费CPU
}
}
}
问题:
- 反复加锁、检查、解锁
- CPU忙等待,性能很差
- 需要更好的方法
正确的解决方案:条件变量
#include <condition_variable>
#include <queue>
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;
queue<int> dataQueue;
mutex mtx;
condition_variable cv; // 条件变量
void producer() {
for (int i = 0; i < 5; i++) {
{
lock_guard<mutex> lock(mtx);
dataQueue.push(i);
cout << "生产: " << i << "\n";
} // 解锁
cv.notify_one(); // 通知一个等待的线程
this_thread::sleep_for(chrono::milliseconds(100));
}
}
void consumer() {
for (int i = 0; i < 5; i++) {
unique_lock<mutex> lock(mtx); // 必须用unique_lock
// 等待队列非空
cv.wait(lock, []{ return !dataQueue.empty(); });
int data = dataQueue.front();
dataQueue.pop();
cout << "消费: " << data << "\n";
}
}
int main() {
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
输出:
生产: 0
消费: 0
生产: 1
消费: 1
生产: 2
消费: 2
生产: 3
消费: 3
生产: 4
消费: 4
完美!生产一个,消费一个!
cv.wait()工作原理详解
这是学习过程中的重点问题!
cv.wait(lock, []{ return !dataQueue.empty(); });
这行代码做了什么?
步骤1:检查条件(lambda表达式)
[]{ return !dataQueue.empty(); } // 条件:队列非空
如果条件为true:
- wait立即返回,继续执行
如果条件为false:
- 进入步骤2
步骤2:释放锁并等待
// wait内部的伪代码逻辑
void wait(unique_lock& lock, Predicate pred) {
while (!pred()) { // 条件不满足
lock.unlock(); // 🔓 自动释放锁!(重要)
// 线程进入等待状态(不占CPU)
// 等待其他线程调用notify_one()或notify_all()
// 被唤醒后
lock.lock(); // 🔒 重新获取锁
// 再次检查条件(防止虚假唤醒)
}
// 条件满足,返回
}
关键理解:
- wait会自动unlock():让其他线程(生产者)能获取锁
- 线程进入睡眠:不占CPU,等待被唤醒
- 被唤醒后重新lock():再次获取锁
- 重新检查条件:防止虚假唤醒
条件变量工作流程图解
这个图展示了条件变量的完整工作流程!
notify_one() vs notify_all()
notify_one():唤醒一个等待的线程
cv.notify_one(); // 只唤醒一个线程
使用场景:
- 只需要一个线程处理
- 例如:生产一个数据,只需要一个消费者
notify_all():唤醒所有等待的线程
cv.notify_all(); // 唤醒所有等待的线程
使用场景:
- 需要所有线程都知道
- 例如:程序退出信号,所有线程都要停止
为什么notify可以在锁外调用?
学习过程中的问题:为什么推荐在锁外notify?
在锁内notify(可能低效)
void producer() {
lock_guard<mutex> lock(mtx); // 上锁
dataQueue.push(data);
cv.notify_one(); // 在锁内通知
} // 这里才解锁
执行过程:
1. 生产者持有锁
2. 生产者notify_one()
3. 消费者被唤醒
4. 消费者尝试lock() → ❌ 阻塞!因为生产者还没解锁
5. 生产者离开作用域,解锁
6. 消费者获得锁,继续执行
问题:消费者醒来后立即又阻塞等锁
在锁外notify(推荐)
void producer() {
{
lock_guard<mutex> lock(mtx);
dataQueue.push(data);
} // 先解锁
cv.notify_one(); // 在锁外通知
}
执行过程:
1. 生产者持有锁
2. 生产者离开作用域,解锁
3. 生产者notify_one()
4. 消费者被唤醒
5. 消费者尝试lock() → ✅ 成功!锁已释放
6. 消费者继续执行
优势:消费者醒来后直接获得锁,避免二次阻塞
为什么wait必须用unique_lock?
wait的工作需要:
lock.unlock(); // 释放锁
// 等待...
lock.lock(); // 重新获取锁
lock_guard不能用:
- ❌ lock_guard没有unlock()方法
- ❌ 不支持手动unlock和lock
unique_lock可以用:
- ✅ unique_lock有unlock()和lock()方法
- ✅ wait内部可以操作锁
这就是为什么条件变量必须配合unique_lock!
虚假唤醒问题
什么是虚假唤醒?
线程可能在没有notify的情况下被唤醒(操作系统行为)
错误的写法:
// ❌ 危险!可能虚假唤醒
cv.wait(lock); // 没有条件检查
// 醒来后,队列可能还是空的!
int data = dataQueue.front(); // 崩溃!
正确的写法:
// ✅ 安全!带条件检查
cv.wait(lock, []{ return !dataQueue.empty(); });
// wait内部会循环检查条件
// while (!条件) { 等待... }
// 即使虚假唤醒,也会重新检查条件
条件变量的核心要点
- 必须配合mutex使用
- 必须用unique_lock(不能用lock_guard)
- 必须有条件检查(lambda表达式)
- notify可以在锁外调用(推荐,避免唤醒后立即阻塞)
🛡️ 核心知识点六:线程安全类设计
什么是线程安全的类?
线程安全:多个线程同时使用这个类的对象,不会出现数据竞争或错误结果
线程不安全的计数器类
class Counter {
private:
int count;
public:
Counter() : count(0) {}
void increment() {
count++; // ❌ 数据竞争!
}
int get() {
return count; // ❌ 读写冲突!
}
};
// 使用
Counter counter;
void worker() {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
}
int main() {
thread t1(worker);
thread t2(worker);
t1.join();
t2.join();
cout << counter.get() << "\n"; // 期望200000,实际可能150000
return 0;
}
线程安全的计数器类
class ThreadSafeCounter {
private:
int count;
mutex mtx; // 成员变量mutex
public:
ThreadSafeCounter() : count(0) {}
void increment() {
lock_guard<mutex> lock(mtx); // 保护count
count++;
}
int get() {
lock_guard<mutex> lock(mtx); // 读也要保护
return count;
}
void reset() {
lock_guard<mutex> lock(mtx);
count = 0;
}
};
使用:
ThreadSafeCounter counter;
void worker() {
for (int i = 0; i < 100000; i++) {
counter.increment(); // 线程安全
}
}
int main() {
thread t1(worker);
thread t2(worker);
t1.join();
t2.join();
cout << counter.get() << "\n"; // 一定是200000
return 0;
}
输出:200000 ✅ 正确!
设计线程安全类的核心原则
原则1:保护所有共享数据
class BankAccount {
private:
int balance;
string owner;
mutex mtx; // 一个mutex保护所有数据
public:
void deposit(int amount) {
lock_guard<mutex> lock(mtx);
balance += amount; // 修改balance需要锁
}
int getBalance() {
lock_guard<mutex> lock(mtx);
return balance; // 读取balance也需要锁
}
string getOwner() {
lock_guard<mutex> lock(mtx);
return owner; // 读取owner也需要锁
}
};
关键:
- 所有成员变量都要保护
- 读操作也要加锁(避免读写冲突)
原则2:不要返回内部数据的引用或指针
学习过程中的问题:为什么返回引用不安全?
❌ 不安全:返回引用
class UserManager {
private:
vector<string> users;
mutex mtx;
public:
// ❌ 危险!返回引用
vector<string>& getUsers() {
lock_guard<mutex> lock(mtx);
return users; // 返回引用
} // 🔓 锁在这里释放了!
};
// 使用
vector<string>& userList = manager.getUsers();
// 💀 危险!现在没有锁保护了
userList.push_back("hacker"); // 其他线程可能同时在修改users
问题:
- 锁只保护了
getUsers()函数内部 - 返回引用后,外部可以随意修改,没有锁保护
- 其他线程可能同时访问 → 数据竞争
✅ 安全方案1:返回副本
vector<string> getUsers() {
lock_guard<mutex> lock(mtx);
return users; // 返回副本,安全
}
✅ 安全方案2:提供安全的访问接口
void forEachUser(function<void(const string&)> func) {
lock_guard<mutex> lock(mtx); // 🔒 上锁
for (const auto& user : users) {
func(user); // 在锁的保护下访问
}
} // 🔓 所有访问结束后才解锁
为什么安全?
- 整个访问过程都在锁的保护下
- 外部无法拿到users的引用
- 其他线程必须等待 → 没有数据竞争
关键:锁要保护整个访问过程,不能只保护一半
线程安全类设计Checklist
✅ 1. 所有成员变量都有mutex保护
✅ 2. 所有public成员函数都加锁
✅ 3. 读操作也要加锁
✅ 4. 不返回内部数据的引用/指针
✅ 5. 原子操作在一个锁内完成
✅ 6. mutex声明为mutable(如果有const函数)
🔒 核心知识点七:死锁问题与避免
什么是死锁?
死锁:两个或多个线程互相等待对方释放锁,导致都无法继续执行
死锁示例
mutex mtx1; // 账户1的锁
mutex mtx2; // 账户2的锁
// 线程1:A转账给B
void transfer_A_to_B() {
mtx1.lock(); // 🔒 锁住账户A
cout << "线程1:锁住A\n";
this_thread::sleep_for(chrono::milliseconds(100));
mtx2.lock(); // 等待锁住账户B
cout << "线程1:锁住B\n";
// 转账操作
mtx2.unlock();
mtx1.unlock();
}
// 线程2:B转账给A
void transfer_B_to_A() {
mtx2.lock(); // 🔒 锁住账户B
cout << "线程2:锁住B\n";
this_thread::sleep_for(chrono::milliseconds(100));
mtx1.lock(); // 等待锁住账户A
cout << "线程2:锁住A\n";
// 转账操作
mtx1.unlock();
mtx2.unlock();
}
死锁发生过程:
时刻1:线程1锁住mtx1 🔒
时刻2:线程2锁住mtx2 🔒
时刻3:线程1想锁mtx2,但被线程2占用 → 等待...
时刻4:线程2想锁mtx1,但被线程1占用 → 等待...
结果:两个线程互相等待,永远卡住!💀
运行结果:
线程1:锁住A
线程2:锁住B
(程序卡死,不再输出)
死锁发生过程图解
避免死锁的方法
方法1:固定加锁顺序
死锁的原因:两个线程以不同顺序加锁
线程1:先锁mtx1,再锁mtx2
线程2:先锁mtx2,再锁mtx1 // 顺序相反!
解决:统一加锁顺序
// ✅ 线程1:固定顺序
void thread1() {
mtx1.lock(); // 先锁1
mtx2.lock(); // 再锁2
// 操作
mtx2.unlock();
mtx1.unlock();
}
// ✅ 线程2:相同顺序
void thread2() {
mtx1.lock(); // 先锁1(和线程1一样)
mtx2.lock(); // 再锁2
// 操作
mtx2.unlock();
mtx1.unlock();
}
结果:不会死锁!
原因:
- 两个线程都先抢mtx1
- 谁先抢到mtx1,谁就能继续抢mtx2
- 另一个线程等待mtx1释放
- 不会互相等待
方法2:使用std::lock()同时加锁
学习过程中的问题:mtx.lock() vs std::lock()的区别?
mtx.lock():单独锁一个mutex
mtx1.lock(); // 只锁mtx1
mtx2.lock(); // 再锁mtx2
// 可能死锁
std::lock():同时锁多个mutex,避免死锁
#include <mutex>
mutex mtx1;
mutex mtx2;
void safeTransfer() {
// 同时锁住mtx1和mtx2,保证不死锁
std::lock(mtx1, mtx2);
// 用adopt_lock告诉lock_guard:锁已经获取了,不要再lock
lock_guard<mutex> lock1(mtx1, adopt_lock);
lock_guard<mutex> lock2(mtx2, adopt_lock);
// 操作
cout << "安全转账\n";
// 自动解锁
}
std::lock()的特点:
- C++11提供的函数
- 一次锁多个mutex
- 自动避免死锁
- 保证:要么全部锁住,要么全部不锁
std::lock()避免死锁的原理图解
关键:要么全成功,要么全失败,避免部分锁住导致死锁
adopt_lock是什么?
学习过程中的问题:adopt_lock干嘛的?为什么又用lock_guard?
adopt_lock 是一个标记(C++11),告诉lock_guard:锁已经lock了,不要再lock
为什么需要adopt_lock?
问题:std::lock()后还要用lock_guard管理解锁
std::lock(mtx1, mtx2); // 已经锁住了
// 但是还要管理解锁!
// 如果中间抛异常,需要手动unlock,很麻烦
mtx1.unlock(); // 可能忘记
mtx2.unlock();
解决:用lock_guard自动管理解锁
std::lock(mtx1, mtx2); // 手动lock
// 用lock_guard管理解锁,但告诉它"已经lock过了"
lock_guard<mutex> lock1(mtx1, adopt_lock); // adopt_lock = 接管已有的锁
lock_guard<mutex> lock2(mtx2, adopt_lock);
// 操作...
// lock_guard析构时会自动unlock
对比:不用adopt_lock会怎样?
std::lock(mtx1, mtx2); // 已经lock了
lock_guard<mutex> lock1(mtx1); // ❌ lock_guard会再次lock!
// 但mtx1已经被lock了 → 死锁!
完整流程
// 步骤1:std::lock同时锁住
std::lock(mtx1, mtx2);
// mtx1: 🔒 已锁
// mtx2: 🔒 已锁
// 步骤2:lock_guard接管(adopt_lock)
lock_guard<mutex> lock1(mtx1, adopt_lock); // 不会再lock,只负责unlock
lock_guard<mutex> lock2(mtx2, adopt_lock);
// 步骤3:操作
cout << "安全操作\n";
// 步骤4:自动解锁(lock_guard析构)
// lock1析构 → mtx1.unlock() 🔓
// lock2析构 → mtx2.unlock() 🔓
总结对比
| 概念 | 作用 | 是否C++11 |
|---|---|---|
mtx.lock() |
单独锁一个mutex | 不是(C++基础) |
std::lock() |
同时锁多个mutex,避免死锁 | ✅ C++11 |
adopt_lock |
告诉lock_guard不要lock,只管unlock | ✅ C++11 |
一句话总结:
std::lock(mtx1, mtx2); // 手动lock(避免死锁)
lock_guard<mutex> lock1(mtx1, adopt_lock); // 自动unlock(异常安全)
// = 手动lock的安全性 + 自动unlock的便利性
⚡ 核心知识点八:std::atomic原子操作
为什么需要atomic?
之前的方法:
int counter = 0;
mutex mtx;
void increment() {
lock_guard<mutex> lock(mtx); // 加锁
counter++;
// 解锁
}
问题:
- 只是
counter++这么简单的操作 - 却要加锁解锁,开销大
- 能不能更高效?
atomic的解决方案
#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
atomic<int> counter(0); // 原子变量,不需要mutex
void increment() {
counter++; // 原子操作,不需要加锁!
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << counter << "\n"; // 输出:2,正确!
return 0;
}
特点:
- ✅ 不需要mutex
- ✅ 不需要加锁
- ✅
counter++是原子操作,硬件级别保证 - ✅ 性能更好
atomic的基本用法
#include <atomic>
// 创建atomic变量
atomic<int> count(0); // 原子int
atomic<bool> flag(false); // 原子bool
atomic<double> value(3.14); // 原子double
// 基本操作
int val = count; // 读取
count = 10; // 写入
count++; // 自增(原子操作)
count--; // 自减
count += 5; // 加法
atomic vs mutex性能对比
用mutex的版本
int counter = 0;
mutex mtx;
void addCount() {
for (int i = 0; i < 1000000; i++) {
lock_guard<mutex> lock(mtx); // 🔒 加锁
counter++;
// 🔓 解锁
}
}
// 100万次加锁解锁!
耗时:约 200ms
用atomic的版本
atomic<int> counter(0);
void addCount() {
for (int i = 0; i < 1000000; i++) {
counter++; // 无锁,硬件原子操作
}
}
// 100万次原子操作
耗时:约 20ms
快了10倍!
atomic vs mutex性能对比图解
graph LR
subgraph atomic操作
A1[counter++] --> A2[硬件原子指令]
A2 --> A3[耗时: 20ns]
end
subgraph mutex操作
M1[lock] --> M2[counter++]
M2 --> M3[unlock]
M3 --> M4[耗时: 200ns]
end
C{选择?} --> |简单操作| atomic操作
C --> |复杂逻辑| mutex操作
style A3 fill:#90EE90
style M4 fill:#FFD700
atomic适合的场景
✅ 适合atomic的场景
// 1. 简单计数器
atomic<int> requestCount(0);
requestCount++;
// 2. 标志位
atomic<bool> stopFlag(false);
if (stopFlag) { /* 停止 */ }
// 3. 简单的状态
atomic<int> status(0);
status = 1; // 改变状态
特点:
- 简单的读写操作
- 不需要复杂的逻辑
- 高性能要求
❌ 不适合atomic的场景
// ❌ 复杂操作:需要多个步骤
atomic<int> balance(1000);
// 这个不是原子的!
if (balance >= amount) { // 步骤1:检查
balance -= amount; // 步骤2:扣款
}
// 两个步骤之间,其他线程可能修改balance
// 正确做法:用mutex
mutex mtx;
lock_guard<mutex> lock(mtx);
if (balance >= amount) {
balance -= amount;
}
特点:
- 需要多步操作
- 有复杂判断
- 要用mutex
atomic vs mutex对比
| 特性 | atomic | mutex |
|---|---|---|
| 性能 | 快(硬件级别) | 慢(系统调用) |
| 适用场景 | 简单操作(++、=) | 复杂操作 |
| 能否保护多个变量 | ❌ 只能一个 | ✅ 可以多个 |
| 能否加条件判断 | ❌ 复杂判断不行 | ✅ 可以 |
| 使用难度 | 简单 | 需要小心死锁 |
选择原则
- atomic:简单计数、标志位 → 高性能
- mutex:复杂逻辑、多变量 → 功能强大
🎯 重要问题解答
学习过程中的关键问题
Q1: cv.wait(lock, []{ return !dataQueue.empty(); })这句代码如何理解?
A: 这行代码做了三件事:
- 检查条件:lambda表达式
!dataQueue.empty()返回true吗? - 如果false:自动unlock()释放锁,线程睡眠等待
- 被唤醒后:重新lock(),再次检查条件,如果true则返回
关键:wait内部会循环检查条件,防止虚假唤醒
Q2: 为什么在锁外notify可以避免唤醒后立即阻塞?
A:
- 锁内notify:生产者还持有锁 → 消费者被唤醒 → 立即阻塞等锁
- 锁外notify:生产者已释放锁 → 消费者被唤醒 → 直接获得锁
- 避免了一次不必要的阻塞等待
Q3: 为什么wait必须用unique_lock而不能用lock_guard?
A:
因为wait内部需要:
lock.unlock()释放锁lock.lock()重新获取锁
lock_guard没有这两个方法,unique_lock有。
Q4: 返回引用为什么不安全?如何理解"安全的访问接口"?
A:
- 不安全原因:返回引用后,锁已释放,外部可以随意修改,没有锁保护
- 安全的接口:整个访问过程都在锁的保护下,外部无法拿到引用
- 关键:锁要保护整个访问过程,不能只保护一半
Q5: function<void(const string&)>是什么语法?
A:
这是C++11的std::function,用于存储可调用对象:
- 在课时04学过
- 可以存储普通函数、lambda表达式、函数对象
- 常用于传递lambda作为参数
Q6: mtx.lock() vs std::lock()的区别?
A:
mtx.lock():单独锁一个mutex,可能死锁std::lock():同时锁多个mutex,自动避免死锁(C++11)std::lock()保证:要么全部锁住,要么全部不锁
Q7: adopt_lock是什么?为什么需要它?
A:
adopt_lock是C++11的标记,告诉lock_guard"锁已经lock了,不要再lock"- 用途:配合std::lock()使用
std::lock()负责安全地加锁(避免死锁)lock_guard负责自动解锁(异常安全)
- 结果:手动lock的安全性 + 自动unlock的便利性
Q8: atomic适合什么场景?什么场景必须用mutex?
A:
- atomic适合:简单的++、–、赋值等单个操作
- mutex适合:复杂的多步操作、条件判断、保护多个变量
- 选择原则:简单操作优先atomic(性能好),复杂逻辑用mutex(功能全)
📝 我的理解
今天学习的收获
1. cv.wait()这行代码终于搞懂了
一开始看到cv.wait(lock, []{ return !dataQueue.empty(); });直接懵了:
- 这啥语法啊,lambda表达式咋用的
- 后来知道wait会自动unlock释放锁,线程睡眠
- 被唤醒后重新lock,再检查条件
- 这个设计太巧妙了,避免了忙等待浪费CPU
关键是理解了wait内部的循环检查,防止虚假唤醒:
while (!条件) {
unlock(); // 释放锁
等待... // 睡眠
lock(); // 重新获取锁
}
2. 在锁外notify的原因
这个当时有点困惑,后来理解了:
- 锁内notify:消费者醒来 → 立即阻塞等锁 → 低效
- 锁外notify:先解锁 → 再唤醒 → 消费者直接获得锁 → 高效
- 避免了"唤醒后立即阻塞"的二次等待
3. function<void(const string&)>这个语法
这个语法之前没见过,一脸懵:
- 后来查了一下,原来是C++11的std::function
- 可以存储任何可调用对象(函数、lambda、函数对象)
- 在课时04学过,但当时没太注意
- 现在知道了,常用于传递lambda作为参数
4. 返回引用为啥不安全终于理解了
之前一直以为返回引用没问题,今天才发现大问题:
- 返回引用后,锁已经释放了
- 外部拿到引用,可以随意修改,没有锁保护
- 其他线程可能同时访问 → 数据竞争
关键理解:锁要保护整个访问过程,不能只保护一半
安全的做法:
- 要么返回副本
- 要么提供安全的访问接口(整个访问过程都在锁内)
5. 死锁的本质和解决方法
死锁这个概念之前听说过,但没深入理解:
- 两个线程互相等对方释放锁 → 永远卡住
- 原因:加锁顺序不一致
解决方法学了两个:
- 方法1:固定加锁顺序(简单粗暴)
- 方法2:用std::lock()同时加锁(更安全)
6. std::lock()和adopt_lock的配合
这个当时完全没懂,后来反复看文档才理解:
std::lock(mtx1, mtx2):手动加锁,避免死锁lock_guard<mutex> lock(mtx1, adopt_lock):自动解锁,异常安全adopt_lock就是告诉lock_guard"别再lock了,已经lock过了"
组合拳:手动lock的安全性 + 自动unlock的便利性
7. atomic的性能优势
这个真是太爽了:
- 简单的counter++,用mutex要100ms
- 用atomic只要10ms,快了10倍!
- 原因:硬件级别的原子操作,不需要系统调用
但是atomic只适合简单操作:
- 复杂的多步骤操作还是要用mutex
- 不能盲目用atomic
还不太理解的地方
1. 虚假唤醒的具体场景
虽然知道要防止虚假唤醒,但具体啥情况下会虚假唤醒还是不太清楚:
- 操作系统的行为?
- 实际项目中会经常遇到吗?
- 需要实际踩坑才能体会
2. mutable mutex是啥意思
线程安全类设计checklist里提到"mutex声明为mutable":
- mutable是啥关键字?
- 为啥mutex要声明为mutable?
- 跟const函数有关系吗?
- 需要进一步学习
实际应用的思考
1. 生产者-消费者模型太实用了
这个模型感觉在很多场景都能用:
- 消息队列
- 任务分发
- 数据缓冲
需要多写几遍代码加深理解
2. 线程安全类设计的checklist很重要
这次学到了设计线程安全类的原则:
- 所有成员变量都要mutex保护
- 所有public函数都要加锁
- 读操作也要加锁
- 不返回引用/指针
- 原子操作在一个锁内完成
感觉这个checklist以后写代码时要经常参考
3. 死锁问题要时刻警惕
虽然学了避免死锁的方法,但感觉实际项目中还是容易踩坑:
- 加锁顺序可能不好控制
- 多个mutex容易乱
- 需要养成好习惯:能用std::lock就用
4. atomic vs mutex的选择
现在有了新的工具atomic,但要知道什么时候用:
- 简单场景:计数器、标志位 → atomic(性能好)
- 复杂场景:多步操作、条件判断 → mutex(功能全)
不能为了性能盲目用atomic,安全第一
整体感受
课时06的下半部分难度明显比上半部分高:
- 条件变量的wait机制很复杂
- 线程安全类设计要考虑很多细节
- 死锁问题需要深入理解
- atomic虽然简单但要知道适用场景
感觉并发编程的水很深,这才刚入门。需要多写代码、多踩坑、多总结经验才能真正掌握。
💡 学习建议:条件变量和线程安全类设计是并发编程的核心,建议多写生产者-消费者模型的练习代码,加深理解。死锁问题要特别注意,实际项目中要始终保持警惕。
🎯 下一步:建议开始做一些综合练习,比如实现线程池、线程安全的消息队列等,将所学知识综合应用。
📝 本文记录了C++并发编程高级主题的学习过程,从条件变量到原子操作,掌握了线程间通信和高性能并发编程的核心技能。记住:正确的并发编程需要深入理解每个工具的工作原理和适用场景!
更多推荐

所有评论(0)