C++ 线程与协程的选择指南:从场景到实践
C++ 线程与协程的选择指南:从场景到实践
一、引言:并发编程的两种范式
线程(Thread)和协程(Coroutine)是 C++ 中实现并发的两种核心机制。线程由操作系统调度,是 CPU 调度的最小单位;协程由程序自身调度,是用户态的“轻量级线程”。选择错误的工具会导致性能瓶颈、资源浪费或代码复杂度失控。
理解两者的本质差异和适用边界,是构建高性能 C++ 应用的基础。
二、核心区别速览
| 维度 | 线程 (std::thread) | 协程 (C++20 Coroutine) |
| --- | --- | --- |
| 调度者 | 操作系统内核(抢占式) | 用户代码/事件循环(协作式) |
| 切换开销 | 高(系统调用、上下文切换,~1-10μs) | 极低(函数调用级别,~10-100ns) |
| 内存开销 | 大(每线程独立栈,通常 1-8MB) | 极小(仅堆上协程帧,通常几十到几百字节) |
| 并发数 | 有限(数百到数千) | 极高(数万到数百万) |
| 并行性 | 真正并行(多核同时执行) | 单线程内并发,非并行 |
| 阻塞操作 | 线程阻塞不影响其他线程 | 协程阻塞会阻塞整个线程 |
| 编程模型 | 复杂(锁、同步、数据竞争) | 相对简单(无锁协作式) |
| 适用场景 | CPU 密集型、阻塞 I/O、需要并行 | 高并发 I/O、非阻塞异步、生成器 |
三、使用线程的场景
3.1 CPU 密集型计算
线程是唯一能实现真正并行计算的方式,可以利用多核 CPU 同时执行。
cpp复制下载
#include <thread>
#include <vector>
#include <numeric>
#include <iostream>
// CPU 密集型:矩阵乘法、图像处理、科学计算
void parallelSum(const std::vector<int>& data,
size_t start, size_t end, long long& result) {
result = std::accumulate(data.begin() + start, data.begin() + end, 0LL);
}
int main() {
std::vector<int> data(10'000'000, 1);
long long sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0;
// 利用 4 核并行计算
size_t chunk = data.size() / 4;
std::thread t1(parallelSum, std::ref(data), 0, chunk, std::ref(sum1));
std::thread t2(parallelSum, std::ref(data), chunk, 2*chunk, std::ref(sum2));
std::thread t3(parallelSum, std::ref(data), 2*chunk, 3*chunk, std::ref(sum3));
std::thread t4(parallelSum, std::ref(data), 3*chunk, data.size(), std::ref(sum4));
t1.join(); t2.join(); t3.join(); t4.join();
std::cout << "Total: " << sum1 + sum2 + sum3 + sum4 << std::endl;
}
3.2 阻塞式系统调用
当操作涉及阻塞式 I/O(如读文件、等待用户输入),线程可以在阻塞时让出 CPU。
cpp复制下载
// 阻塞 I/O:文件读写、socket 阻塞模式、数据库查询
void blockingFileRead(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
std::vector<char> buffer(1024 * 1024); // 1MB
file.read(buffer.data(), buffer.size()); // 阻塞操作,但不影响其他线程
// 处理数据...
}
std::thread readerThread(blockingFileRead, "large_file.bin");
// 主线程可以做其他事情
doOtherWork();
readerThread.join();
3.3 需要与现有同步模型集成
cpp复制下载
// 使用 condition_variable 实现生产者-消费者
class ThreadSafeQueue {
std::queue<int> queue_;
mutable std::mutex mtx_;
std::condition_variable cv_;
public:
void push(int value) {
std::lock_guard lock(mtx_);
queue_.push(value);
cv_.notify_one(); // 唤醒等待的线程
}
int pop() {
std::unique_lock lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty(); });
int value = queue_.front();
queue_.pop();
return value;
}
};
四、使用协程的场景
4.1 高并发 I/O 密集型服务
协程最适合处理大量并发连接的网络服务,避免为每个连接创建线程。
cpp复制下载
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
using boost::asio::awaitable;
using boost::asio::co_spawn;
using boost::asio::use_awaitable;
// 协程处理单个客户端连接
awaitable<void> handleClient(tcp::socket socket) {
try {
char buffer[1024];
while (true) {
// 异步读取,不阻塞线程
size_t n = co_await socket.async_read_some(
boost::asio::buffer(buffer), use_awaitable);
// 异步写入
co_await async_write(socket,
boost::asio::buffer(buffer, n), use_awaitable);
}
} catch (const std::exception& e) {
std::cerr << "Client error: " << e.what() << std::endl;
}
}
// 主协程:接受连接
awaitable<void> listener(tcp::acceptor& acceptor) {
while (true) {
auto socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(acceptor.get_executor(),
handleClient(std::move(socket)), detached);
}
}
int main() {
boost::asio::io_context ioContext;
tcp::acceptor acceptor(ioContext, tcp::endpoint(tcp::v4(), 8080));
co_spawn(ioContext, listener(acceptor), detached);
ioContext.run(); // 事件循环驱动所有协程
}
性能对比:在 10 万并发连接场景下,使用线程可能需要 10 万个线程(每个 8MB 栈 = 800GB 内存),而协程仅需 10 万个协程帧(每个约 100 字节 = 10MB 内存)。
4.2 生成器和惰性求值
协程可以自然地表达惰性序列,按需产生值而非一次性计算全部结果。
cpp复制下载
#include <coroutine>
#include <iostream>
#include <optional>
// 生成器:按需产生斐波那契数列
template<typename T>
struct Generator {
struct promise_type {
T currentValue;
auto get_return_object() { return Generator{this}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
auto yield_value(T value) {
currentValue = value;
return std::suspend_always{};
}
void return_void() { }
void unhandled_exception() { std::terminate(); }
};
struct iterator {
std::coroutine_handle<promise_type> handle;
bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
iterator& operator++() { handle.resume(); return *this; }
T operator*() const { return handle.promise().currentValue; }
};
iterator begin() { handle.resume(); return {handle}; }
std::default_sentinel_t end() { return {}; }
private:
explicit Generator(promise_type* p)
: handle(std::coroutine_handle<promise_type>::from_promise(*p)) { }
std::coroutine_handle<promise_type> handle;
};
// 斐波那契数列生成器
Generator<int> fibonacci(int limit) {
int a = 0, b = 1;
for (int i = 0; i < limit; ++i) {
co_yield a;
int temp = a;
a = b;
b = temp + b;
}
}
int main() {
// 惰性生成:只在需要时计算下一个值
for (int value : fibonacci(10)) {
std::cout << value << " "; // 0 1 1 2 3 5 8 13 21 34
}
}
4.3 异步操作的顺序编排
协程用同步风格编写异步代码,避免“回调地狱”。
cpp复制下载
// 协程:像同步代码一样编排异步操作
awaitable<void> processUserData(const std::string& userId) {
// 顺序执行多个异步操作,代码风格像同步
auto userInfo = co_await fetchUserInfo(userId); // 异步查询用户
auto orders = co_await fetchUserOrders(userId); // 异步查询订单
auto recommendations = co_await fetchRecommendations(userInfo); // 异步推荐
// 处理结果
renderPage(userInfo, orders, recommendations);
}
五、选择决策流程
图表代码下载全屏
六、混合使用:线程 + 协程
在实际项目中,线程和协程经常混合使用,发挥各自优势:
cpp复制下载
// 多线程 + 每线程内协程:充分利用多核 + 高并发 I/O
class HybridServer {
std::vector<std::thread> workerThreads_;
void workerLoop(boost::asio::io_context& ioCtx) {
ioCtx.run(); // 事件循环驱动协程
}
public:
void start(int numThreads) {
boost::asio::io_context ioCtx;
// 在事件循环上启动协程
co_spawn(ioCtx, listener(ioCtx), detached);
// 启动多个工作线程(通常等于 CPU 核心数)
for (int i = 0; i < numThreads; ++i) {
workerThreads_.emplace_back(&HybridServer::workerLoop,
this, std::ref(ioCtx));
}
}
};
// 这种设计在 Nginx、Redis、Seastar 等高性能系统中广泛应用
七、总结
线程和协程的选择可以归纳为以下核心原则:
- 线程用于 CPU 密集和真正并行:需要利用多核 CPU 的计算能力时必须使用线程。线程由操作系统抢占式调度,适合计算密集型任务、阻塞式系统调用和需要与现有同步模型集成的场景。缺点是内存开销大(每线程独立栈)、切换代价高。
- 协程用于高并发 I/O 和异步编排:协程在用户态通过事件循环协作式调度,切换代价极低,内存占用小。适合高并发网络服务、生成器/惰性求值和异步操作的顺序编排。缺点是不提供真正并行,协程内的阻塞操作会阻塞整个线程。
- 混合使用是现代高性能系统的最佳实践:使用少量线程(通常等于 CPU 核数)充分利用多核,每个线程内运行大量协程处理海量并发连接。这种架构兼具多核并行和高并发 I/O 的优势。
- 选择的关键因素:
- 任务性质:CPU 密集型 → 线程;I/O 密集型 → 协程
- 并发规模:低并发 → 线程池;高并发(万级+)→ 协程
- 编程复杂度:同步阻塞代码 → 线程;异步非阻塞 → 协程
- 资源限制:内存紧张 → 协程;内存充裕 → 线程也可接受
线程和协程并非互斥,而是互补。在高性能 C++ 应用中,合理的架构往往是“少量线程 + 大量协程”的组合,各取所长。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)