致并发的新大陆01

CPP异步编程快速入门

Posted by Miso on February 23, 2026

-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. 结语


迈入异步编程的门槛,我们完成了一次从同步思维到异步思维的转变。让我们回顾这段旅程:

收获与总结

  1. 理解了并发编程的必要性
    • 程序大量时间在”等待”:I/O操作、用户输入、网络响应
    • 多核CPU的潜力需要并发编程来释放
    • 异步编程让CPU在等待时做有意义的事情
  2. 掌握了async-future模式的核心
    • std::async:启动异步任务的”发射按钮”
    • std::future:异步结果的”提货单”
    • future.get():等待结果并确保任务完成的”汇合点”
  3. 体验了从直线到分支的转变
    1
    2
    3
    4
    5
    
    // 之前:一条直线到底
    A → B → C → D
       
    // 之后:优雅的双分支
    A → B → {主分支, 侧分支} → 汇合 → D
    

下一步学习方向

本文介绍的 async/future 只是C++并发编程的入门。当你掌握了这个基础后,可以继续探索:

  1. 更复杂的并发模式
    • std::promise:手动控制异步结果
    • std::packaged_task:将函数包装为异步任务
    • 线程池:管理大量并发任务
  2. 同步原语
    • 互斥锁(std::mutex):保护共享数据
    • 条件变量(std::condition_variable):线程间通信
    • 原子操作(std::atomic):无锁编程
  3. 现代并发特性
    • C++20:协程(coroutines)——异步编程的终极形态

如此,欢迎各位来到新大陆