一、进程与线程的核心区别
1. 不同系统下的调度模型
| 系统 | 进程角色 | 线程角色 |
|---|---|---|
| Windows | 资源分配单位(拥有代码/数据/堆/栈) | 调度执行单位(由 main 函数创建主线程) |
| Linux | 既是资源分配单位,也是可调度实体 | 轻量级进程,同样可调度,共享进程资源 |
2. 关键差异对比
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源共享 | 进程间完全隔离,需通过 IPC(管道/消息队列)通信 | 共享同一进程的代码区、数据区、堆区,仅栈和寄存器独立 |
| 切换开销 | 大(需保存完整上下文) | 小(仅切换栈和寄存器) |
| 崩溃影响 | 单个进程崩溃不影响其他进程 | 任意线程崩溃会导致整个进程终止 |
补充知识点:
-
std::thread::hardware_concurrency()可获取 CPU 逻辑核心数 -
线程的内核栈在进程的堆区分配,代码仍在代码区(地址不变)
-
一个 main 函数就是一个进程
二、Lambda 表达式深度解析
1. 完整语法与组成
cs
[capture] (params) opt -> ret { body; }
| 部分 | 说明 |
|---|---|
[capture] |
捕获列表(决定外部变量访问方式) |
(params) |
参数列表 |
opt |
函数选项(如 mutable) |
-> ret |
返回值类型(可自动推导) |
{ body } |
函数体 |
2. 捕获列表详解
| 捕获方式 | 行为说明 |
|---|---|
[] |
不捕获任何变量 |
[&] |
按引用捕获所有外部变量(可修改原变量) |
[=] |
按值捕获所有外部变量(默认是 const 副本,不可修改) |
[=, &foo] |
按值捕获所有变量,按引用捕获 foo |
[bar] |
仅按值捕获 bar |
[this] |
捕获当前类的 this 指针,可访问成员变量/函数 |
关键点:
-
引用捕获的生命周期风险:捕获的引用必须在 Lambda 执行时仍有效,否则会出现悬空引用
-
嵌套捕获规则:内层 Lambda 默认只能捕获外层 Lambda 的变量,而非直接捕获最外层的变量
-
类成员捕获 :
[=]/[&]会自动捕获this,可直接访问类成员
3.值捕获的可修改性
默认情况下,值捕获的变量在 Lambda 内部是 const 副本 ,不能修改。如果要修改,必须加 mutable 关键字,且修改的是副本,不影响原变量。
cpp
int x = 10;
// 不加 mutable:编译错误
auto f1 = [=]() { x += 10; }; // ❌ 错误:不能修改 const 副本
// 加 mutable:可以修改副本
auto f2 = [=]() mutable { x += 10; }; // ✅ 修改的是副本,原 x 不变
f2();
cout << x << endl; // 输出 10,原变量未改变
关键点:
-
mutable去掉的是const属性,让副本可以被修改 -
修改的是 Lambda 内部存储的副本,不是外部的原变量
-
即使加了
mutable,外部变量也不会改变
4. 返回值自动推导规则
规则1:单条 return 语句可自动推导
cpp
auto f1 = [](int i) { return i; }; // ✅ 推导为 int
auto f2 = [](double d) { return d; }; // ✅ 推导为 double
auto f3 = [](int a, int b) { return a + b; }; // ✅ 推导为 int
规则2:多条 return 语句且类型一致,可自动推导
cpp
auto f4 = [](int x) {
if (x > 0) return 1;
else return -1; // 都是 int,✅ 推导为 int
};
规则3:多条 return 语句类型不一致,编译错误
cpp
auto f5 = [](int x) {
if (x > 0) return 1;
else return 1.0; // ❌ 错误:int 和 double 冲突,无法推导
};
规则4:初始化列表 {} 无法推导,必须显式指定
cpp
auto f6 = []() { return {1, 2, 3}; }; // ❌ 错误:无法推导
auto f7 = []() -> vector<int> { return {1, 2, 3}; }; // ✅ 正确
原因 :{1, 2, 3} 可以是 vector<int>、list<int>、array<int,3> 等多种类型,编译器不知道你要哪一种,所以必须显式指定。
5. Lambda 本质
编译器会为每个 Lambda 生成一个匿名类 ,重载 operator(),捕获的变量会作为类的成员存储:
-
值捕获 → 成员变量是拷贝副本
-
引用捕获 → 成员变量是引用
Lambda 本质是一个带状态的可调用对象。
三、条件变量 std::condition_variable 高级用法
1. 带谓词的 wait
cv.wait(lock, pred) 等价于:
cpp
while (!pred()) {
cv.wait(lock);
}
pred 是返回 bool 的可调用对象(常为 Lambda),作用:防止虚假唤醒,只有 pred() 为 true 时才会继续执行。
示例:
cpp
// 写法1:手动 while 循环
while (i % 2 != 0) { cv.wait(lock); }
// 写法2:带谓词 wait(等价)
cv.wait(lock, []() { return i % 2 == 0; });
2. 生产者-消费者模型(简单版)
场景:一个生产者生产数据,一个消费者消费数据,两者交替执行。
核心思路 :用 bool tag 标志当前应该生产还是消费,配合条件变量实现交替。
cpp
int num = -1;
const int n = 10;
bool tag = true; // true:应该生产,false:应该消费
mutex mtx;
condition_variable cv;
void Producer() {
unique_lock<mutex> lock(mtx);
for (int i = 0; i < n; ++i) {
// 等待 tag == true(轮到生产)
cv.wait(lock, [] { return tag; });
num = i;
cout << "生产数据:" << num << endl;
tag = false; // 生产完,轮到消费
cv.notify_one(); // 唤醒消费者
}
}
void Consumer() {
unique_lock<mutex> lock(mtx);
for (int i = 0; i < n; ++i) {
// 等待 tag == false(轮到消费)
cv.wait(lock, [] { return !tag; });
cout << "消费数据:" << num << endl;
num = -1;
tag = true; // 消费完,轮到生产
cv.notify_one(); // 唤醒生产者
}
}
执行流程:
-
生产者先执行,生产数据,
tag变为false -
消费者被唤醒,消费数据,
tag变为true -
生产者被唤醒,继续生产... 循环 10 次
3. 封装同步队列(通用版)
场景:多个生产者、多个消费者共享一个固定大小的队列。队列满时生产者等待,队列空时消费者等待。
核心设计:
-
put():生产者放数据,队列满则等待 -
get():消费者取数据,队列空则等待
cpp
#include <list>
#include <mutex>
#include <condition_variable>
using namespace std;
template<class _Ty>
class SyncQueue {
private:
list<_Ty> qu;
static const int maxsize = 8; // 队列最大容量
mutex m_mutex;
condition_variable m_cv;
bool IsFull() const { return qu.size() >= maxsize; }
bool IsEmpty() const { return qu.empty(); }
public:
// 生产者放数据
void put(const _Ty& val) {
unique_lock<mutex> lock(m_mutex);
// 队列满则等待
m_cv.wait(lock, [this] { return !IsFull(); });
qu.push_back(val);
m_cv.notify_all(); // 唤醒消费者
}
// 消费者取数据
void get(_Ty& val) {
unique_lock<mutex> lock(m_mutex);
// 队列空则等待
m_cv.wait(lock, [this] { return !IsEmpty(); });
val = qu.front();
qu.pop_front();
m_cv.notify_all(); // 唤醒生产者
}
};
const int n = 100;
void Producer(SyncQueue<int>& qu) {
for (int i = 0; i < n; ++i) {
cout << "生产:" << i << endl;
qu.put(i);
}
}
void Consumer(SyncQueue<int>& qu) {
for (int i = 0; i < n; ++i) {
int val;
qu.get(val);
cout << "消费:" << val << endl;
}
}
int main() {
SyncQueue<int> myqu;
thread prod(Producer, ref(myqu));
thread cons(Consumer, ref(myqu));
prod.join();
cons.join();
return 0;
}
关键点:
-
wait(lock, predicate)中的谓词[this] { return !IsFull(); }确保只有队列不满时才继续 -
notify_all()唤醒所有等待线程(生产者和消费者都可能被唤醒) -
使用
std::ref传递同步队列的引用,避免拷贝
四、死锁问题与解决方案
1.死锁的核心成因
死锁是多线程编程中典型的并发问题。本文以银行双向转账为例:
-
线程1:从账户A转给账户B,先锁A,再锁B
-
线程2:从账户B转给账户A,先锁B,再锁A
当两个线程同时执行时,线程1持有A锁等待B锁,线程2持有B锁等待A锁,双方互相等待,形成循环等待,程序永久阻塞。
关键点:单个互斥锁不会死锁,死锁一定发生在多锁竞争、加锁顺序不一致的场景。
2.错误代码(会死锁)
cpp
void transferBad(Account& from, Account& to, int money) {
unique_lock<mutex> lockFrom(from.getMutex());
unique_lock<mutex> lockTo(to.getMutex()); // 加锁顺序不固定,容易死锁
if (from.getMoney() >= money) {
from.subMoney(money);
to.addMoney(money);
}
}
3.解决方案一:std::lock 批量加锁
核心思想:要么同时拿到所有锁,要么一把都不拿,彻底打破循环等待。
cpp
void transfer(Account& from, Account& to, int money) {
// 批量同时加锁,要么全拿,要么不拿
lock(from.getMutex(), to.getMutex());
// adopt_lock:表示锁已持有,lock_guard 只负责自动解锁
lock_guard<mutex> lock1(from.getMutex(), adopt_lock);
lock_guard<mutex> lock2(to.getMutex(), adopt_lock);
if (from.getMoney() >= money) {
from.subMoney(money);
to.addMoney(money);
cout << from.getName() << "给" << to.getName() << "转账:" << money << "元" << endl;
} else {
cout << "余额不足" << endl;
}
}
关键点:
-
std::lock:批量加锁,保证原子性 -
adopt_lock:告诉lock_guard锁已持有,只需负责解锁 -
必须配合使用,缺一不可
4.解决方案二:单例模式 + 集合管理账户
核心思路:全局只有一个账户管理实例,转账前必须先申请账户使用权,申请成功才能转账,否则阻塞等待。
set 区分账户的关键问题:
set 默认通过 operator< 判断元素是否相同。如果只比较余额,两个不同账户(余额相同)会被视为同一个元素,无法同时插入。
解决方法:
| 方法 | 做法 | 说明 |
|---|---|---|
| 按地址区分 | set<Account*> 存储指针 |
每个对象地址唯一,天然区分 |
| 按账户名区分 | 重载 < 时比较 m_name |
按业务逻辑区分,更语义化 |
cpp
// 按账户名区分(推荐)
bool operator<(const Account& src) const {
return this->m_name < src.m_name;
}
完整代码:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <set>
#include <string>
using namespace std;
class Account {
private:
string m_name;
int m_money;
mutex m_mtx;
public:
Account(string name, int money) : m_name(name), m_money(money) {}
string getName() const { return m_name; }
int getMoney() const { return m_money; }
void addMoney(int money) { m_money += money; }
void subMoney(int money) { m_money -= money; }
mutex& getMutex() { return m_mtx; }
// 按账户名区分,解决余额相同无法插入 set 的问题
bool operator<(const Account& src) const {
return this->m_name < src.m_name;
}
};
class AcCountManager {
private:
AcCountManager() = default;
AcCountManager(const AcCountManager&) = delete;
AcCountManager& operator=(const AcCountManager&) = delete;
set<Account> accountSet;
mutex m_mtx;
condition_variable m_cv;
public:
static AcCountManager* getInstance() {
static AcCountManager instance;
return &instance;
}
void applyAccount(Account& from, Account& to) {
unique_lock<mutex> lock(m_mtx);
while (accountSet.count(from) > 0 || accountSet.count(to) > 0) {
m_cv.wait(lock);
}
accountSet.insert(from);
accountSet.insert(to);
}
void freeAccount(Account& from, Account& to) {
unique_lock<mutex> lock(m_mtx);
accountSet.erase(from);
accountSet.erase(to);
m_cv.notify_all();
}
};
void transfer(Account& from, Account& to, int money) {
AcCountManager* manager = AcCountManager::getInstance();
manager->applyAccount(from, to);
lock(from.getMutex(), to.getMutex());
lock_guard<mutex> lock1(from.getMutex(), adopt_lock);
lock_guard<mutex> lock2(to.getMutex(), adopt_lock);
if (from.getMoney() >= money) {
from.subMoney(money);
to.addMoney(money);
cout << from.getName() << "给" << to.getName() << "转账:" << money << "元" << endl;
} else {
cout << from.getName() << "余额不足" << endl;
}
manager->freeAccount(from, to);
}
int main() {
Account user1("火龙果", 5000);
Account user2("菠萝", 10000);
thread t1(transfer, ref(user1), ref(user2), 1000);
thread t2(transfer, ref(user2), ref(user1), 500);
t1.join();
t2.join();
cout << user1.getName() << "余额:" << user1.getMoney() << endl;
cout << user2.getName() << "余额:" << user2.getMoney() << endl;
return 0;
}
5.常见疑问解答
疑问1:std::lock 方案中,lock_guard 没有加锁,为什么还要写?
std::lock 已经完成了加锁操作。lock_guard 配合 adopt_lock 的作用是自动解锁 ,确保离开作用域时锁一定会被释放,避免忘记 unlock() 导致死锁,同时保证异常安全。
cpp
lock(from.getMutex(), to.getMutex()); // 加锁
lock_guard<mutex> lock1(from.getMutex(), adopt_lock); // 只负责解锁
lock_guard<mutex> lock2(to.getMutex(), adopt_lock); // 只负责解锁
疑问2:方案一是两个线程一个管A、一个管B吗?
不是。 方案一是每个线程自己同时锁住两个账户,不是分别管理。
-
错误理解:线程1管A,线程2管B
-
正确理解:每个想转账的线程,必须自己一次性拿到A和B两把锁
cpp
// 线程A执行转账:必须同时锁住A和B
thread t1(transfer, A, B, 100);
thread t2(transfer, B, A, 200); // 同样需要同时锁住B和A
疑问3:方案二是只有一个线程在管理吗?
不是。 方案二是通过**管理员(单例对象)**来分配账户的使用权,不是只有一个线程。
-
可以有多个线程同时向管理员申请
-
管理员保证同一时刻只有一个线程能拿到A和B
-
其他线程阻塞等待,直到释放后才能申请成功
cpp
// 多个线程可以同时调用转账
thread t1(transfer, A, B, 100); // 申请A和B
thread t2(transfer, A, B, 200); // 阻塞等待,直到t1释放
thread t3(transfer, C, D, 300); // 不同账户,可以并行
疑问4:两种方案都是同一时刻只有一个线程能操作A和B吗?
是的。 这是两种方案的共同目标,也是避免死锁的关键。
| 方案 | 如何保证 | 同一时刻操作A和B的线程数 |
|---|---|---|
| 方案一 | 线程自己一次性拿两把锁 | 1个 |
| 方案二 | 管理员分配账户使用权 | 1个 |
疑问5:不同账户的转账能同时进行吗?
可以。 两种方案锁的都是具体账户,不是锁整个函数。
cpp
thread t1(transfer, A, B, 100); // 操作A和B
thread t2(transfer, C, D, 200); // 操作C和D,与t1并行执行,互不影响
五、总结
| 知识点 | 核心要点 |
|---|---|
| 进程 vs 线程 | 进程资源隔离,线程共享资源;线程崩溃导致进程终止 |
| Lambda 捕获 | [=] 值捕获是 const 副本,需 mutable 修改;[&] 引用捕获注意生命周期 |
| Lambda 本质 | 编译器生成匿名类,捕获变量作为成员存储 |
| 条件变量 | wait(lock, pred) 带谓词可防止虚假唤醒 |
| 生产者-消费者 | 用条件变量实现生产/消费同步,队列满/空时阻塞 |
| 死锁原因 | 多锁竞争 + 加锁顺序不一致 + 循环等待 |
| 死锁解决方案一 | std::lock 批量加锁 + adopt_lock,一次性获取所有锁 |
| 死锁解决方案二 | 单例 + set 集合管理账户,管理员分配使用权 |
| set 容器 | 元素唯一、自动排序、查找快,用于记录正在使用的账户 |
| adopt_lock | 告诉 lock_guard 锁已持有,只负责解锁 |
| 不同账户转账 | 可以并行执行,互不影响 |