C++中的并发:从线程到协程和async/await

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:为什么C++中的并发并不可怕,反而很有趣?



很久以前,在单核处理器的时代,程序都是严格按顺序执行的。而今天,即便是你的智能手机也拥有8个核心,服务器更是有几十个核心。让所有这些核心为你工作——这才是性能的真正魔力所在。而C++,作为最强大且最底层的语言之一,为我们提供了实现这一目标的一整套工具。但如何不迷失其中呢?线程、协程(常被类比Go语言称为“goroutine”)、async/await——该如何选择?



在本文中,我们将系统地梳理现代C++中的并发。你将了解何时该使用经典的线程,何时该使用轻量级的C++20协程,以及async/await如何简化异步编程。让我们开始吧!



经典方案:使用std::thread的多线程



我们从最直观的——线程开始讲起。std::thread是对操作系统系统线程的直接访问。每个线程都是一个独立的执行上下文,可以在单独的CPU核心上运行。



简单示例:并行计算



假设你需要计算从1到1000万的数字的平方和。按顺序计算会很慢。让我们来并行处理:



#include <iostream>#include <thread>#include <vector>#include <numeric>

void sum_squares(int start, int end, long long& result) { result = 0; for (int i = start; i <= end; ++i) { result += i * i; }}

int main() { const int N = 10'000'000; const int NUM_THREADS = 4; std::vector<std::thread> threads; std::vector<long long> results(NUM_THREADS, 0); int chunk = N / NUM_THREADS; for (int i = 0; i < NUM_THREADS; ++i) { int start = i * chunk + 1; int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk; threads.emplace_back(sum_squares, start, end, std::ref(results[i])); } for (auto& t : threads) { t.join(); } long long total = std::accumulate(results.begin(), results.end(), 0LL); std::cout << "平方和: " << total << std::endl; return 0;}


这里我们创建了4个线程,每个线程计算自己那部分范围。结果收集到一个向量中。一切简单且可预测。但有一些注意事项:



  • 开销大:创建每个线程的成本很高(栈分配、系统调用)。对于成千上万的小任务来说,这效率不高。
  • 同步问题:如果线程访问共享数据,则需要互斥锁,这可能导致阻塞并降低性能。
  • 数量有限:线程数量通常不应超过CPU核心数(考虑超线程)。


对于繁重、长时间的计算,线程是绝佳选择。但对于I/O操作(文件读取、网络请求),它们往往过于笨重。这时,协程就登场了。



C++20的革命:协程(goroutine)



C++20中的协程不仅仅是一个新特性。它从根本上改变了我们编写异步代码的方式。与线程不同,协程是轻量级的协程,可以暂停执行并在稍后恢复,而不会阻塞操作系统线程。



许多开发者称它们为“goroutine”,因为与Go语言中的goroutine相似,尽管技术上它们是不同的概念。主要优势在于:你可以在一个线程上启动数十万个协程而不会损失性能。



它是如何工作的?



协程是一个包含关键字co_await、co_return或co_yield的函数。编译器会自动将其转换为一个有限状态机。以下是一个基本示例:



#include <iostream>#include <coroutine>#incl                

博客

书籍推荐