-1. 系列前言 我们为什么需要并发编程
想象我们正在咖啡店点单,店员接了单子,然后就站在那儿盯着咖啡机,等咖啡做好了才去收下一个人的钱。其他的店员们满脸微笑地站在那里。整个队伍都堵死了。
这就是单线程顺序执行的样子。
并发编程的创立基于两个极其现实的因素:
- 时间不能浪费在等待上。
- 程序跑起来,有大量时间其实是在”等”——等硬盘读文件、等网络返回数据、等用户点击按钮。CPU 的闲置期就这样被浪费了。
- 并发让我们在等待的间隙,去做别的有意义的事情。
- 多核 CPU最好全部用上。
- 你的电脑可能有 8 个核,但单线程程序只能利用其中一个。剩下 7 个显然正在放假。
- 并发编程可以让我们用上尽可能多的CPU。
0. 关于本文
本文将讲述C++并发编程中的重要模型——异步编程模型中的async-future模式。我们将讨论async和future如何将单一线性的执行过程转变为多分支互不干扰的执行过程。
1. 场景预设
这里,我们建立了一套小小的函数来模拟可用并发编程的小程序。四个函数分别是:询问原价、计算打折、生成复杂验证码和按回车确认。其中,生成复杂验证码故意被设计为时间瓶颈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int ask_for_original_price();
double calculate_discounted_price(const int original_price);
string calculate_verification_code(const int original_price) {
// 复杂验证码生成
string code = to_string(original_price * 73 % 107) + "123";
// 复杂耗时计算:进行大量模乘运算
const long long iterations = 200'000'000; // 根据实际机器性能可调整
volatile long long dummy = 1; // volatile 防止优化
for (long long i = 1; i <= iterations; ++i) {
dummy = (dummy * i) % 1'000'000'007; // 模一个大质数
}
// 用 dummy 的值做一个看似有意义的操作(但不影响返回值)
if (dummy == 0) {
code += "overflow"; // 永远不会执行,但防止编译器完全忽略 dummy
}
return code;
}
void ask_for_permission() {
cout << "Please press enter to continue..." << endl;
cin.ignore(); // 清空残留的换行符
cin.get();
}
int main() {
int original_price = ask_for_original_price();
double discounted_price = calculate_discounted_price(original_price);
string verification_code = calculate_verification_code(original_price);
ask_for_permission();
cout << "Discounted price: " << discounted_price << endl;
cout << "Verification code: " << verification_code << endl;
cout << "Thank you for using our service!" << endl;
}
在顺序执行的情况下,用户在输入原价后会感知到一个明显的卡顿,之后才出现按键确认环节。若把验证码生成函数移到按键确认之后,则在按键确认后有着明显卡顿。
可见,在一条向下的直线一样的执行顺序下,这个卡顿永远不可避免。但是,如果我们将验证码生成的耗时和用户按下回车的耗时叠加到一起,让CPU在等待用户的同时干点有意义的事,就可以有效消除卡顿感。此时,程序执行的流程从向下的直线变为了向下的双分支。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
+-------+
| Start |
+-------+
|
v
+----------------------+
|ask_for_original_price|
+----------------------+
|
v
+--------------------------+
|calculate_discounted_price|
+--------------------------+
|
v
+-------+
| Fork | <-- 分叉,同时执行两个任务
+-------+
|
+---------------------------+
| |
v v
+-------------------+ +----------------------------+
|ask_for_permission | |calculate_verification_code |
| (等待用户按键) | | (CPU计算) |
+-------------------+ +----------------------------+
| |
| |
+------------+--------------+
|
v
+-------+
| get | <-- 两者都完成后汇合
+-------+
|
v
+----------------+
| output |
+----------------+
|
v
+-----+
| End |
+-----+
插图由DeepSeek生成
后文将把CPU计算分支称为侧分支,将等待用户分支称为主分支。
2. 启动分支
分支前操作完全不变。
1
2
3
4
void async_main() {
// 分支前操作
int original_price = ask_for_original_price();
double discounted_price = calculate_discounted_price(original_price);
为了启动侧分支,我们需要使用std::async函数。
1
2
3
4
5
auto future_verification_code = async(
launch::async,
calculate_verification_code,
original_price
);
这个函数的首个参数称为启动策略,它确定了侧分支的CPU计算是立刻开始还是侧分支合并时才进行。标准库内置了一个枚举来给我们选择:enum class launch{async = 1, deferred = 2};这里选择async来让分支计算立刻开始。
future_verification_code将承载由async返回的future对象,其用途会在合并分支时讲述。
主分支在侧分支启动后继续书写。总览如下:
1
2
3
4
5
6
7
8
9
10
11
// 分支前操作
int original_price = ask_for_original_price();
double discounted_price = calculate_discounted_price(original_price);
// 侧分支
auto future_verification_code = async(
launch::async,
calculate_verification_code,
original_price
);
// 主分支
ask_for_permission();
请记住,此时侧分支的运算和主分支的等待将同步开始。
3. 合并分支
收尾信息打印将在两个分支全部进行完毕后进行。将打印信息的代码写在ask_for_permission();的后方,便可以保证主分支已经顺利结束。但是,我们如何保证侧分支结束呢?
C++的future对象提供了一个(掌声响起来)非常方便的方法future.get(),来确保侧分支运行完毕,并取得侧分支的最终结果。如果主分支等待时间较短,调用future.get()时侧分支运算尚未完成,则阻塞调用get()的线程来保证侧分支执行完毕。
我们在ask_for_permission()后写上string verification_code = future_verification_code.get();,就能在确保侧分支结束之后获取其结果。
总览如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void async_main() {
// 分支前操作
int original_price = ask_for_original_price();
double discounted_price = calculate_discounted_price(original_price);
// 侧分支
auto future_verification_code = async(
launch::async,
calculate_verification_code,
original_price
);
// 主分支
ask_for_permission();
// 分支合并
string verification_code = future_verification_code.get();
// 合并后操作
cout << "Discounted price: " << discounted_price << endl;
cout << "Verification code: " << verification_code << endl;
cout << "Thank you for using our service!" << endl;
}
4. 结语
迈入异步编程的门槛,我们完成了一次从同步思维到异步思维的转变。让我们回顾这段旅程:
收获与总结
- 理解了并发编程的必要性
- 程序大量时间在”等待”:I/O操作、用户输入、网络响应
- 多核CPU的潜力需要并发编程来释放
- 异步编程让CPU在等待时做有意义的事情
- 掌握了async-future模式的核心
std::async:启动异步任务的”发射按钮”std::future:异步结果的”提货单”future.get():等待结果并确保任务完成的”汇合点”
- 体验了从直线到分支的转变
1 2 3 4 5
// 之前:一条直线到底 A → B → C → D // 之后:优雅的双分支 A → B → {主分支, 侧分支} → 汇合 → D
下一步学习方向
本文介绍的 async/future 只是C++并发编程的入门。当你掌握了这个基础后,可以继续探索:
- 更复杂的并发模式
std::promise:手动控制异步结果std::packaged_task:将函数包装为异步任务- 线程池:管理大量并发任务
- 同步原语
- 互斥锁(
std::mutex):保护共享数据 - 条件变量(
std::condition_variable):线程间通信 - 原子操作(
std::atomic):无锁编程
- 互斥锁(
- 现代并发特性
- C++20:协程(coroutines)——异步编程的终极形态
如此,欢迎各位来到新大陆