C++20 引入了多种工具,使得并发编程变得更直观、更加安全。本文从标准库提供的 std::thread、std::async、std::future 等基本构件谈起,逐步过渡到更高级的 std::jthread、协程(std::coroutine)以及与第三方库的结合,帮助读者快速掌握现代并发技术的核心思想与实战技巧。
1. 基础线程与同步
1.1 std::thread 的基本使用
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Thread " << id << " started\n";
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " finished\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
}
std::thread 提供了最直接的线程创建方式,但它的生命周期管理不够友好。若忘记 join() 或 detach(),程序会抛出异常。
1.2 互斥与条件变量
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void producer() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 处理数据
}
使用 std::unique_lock 与 std::condition_variable 可以实现线程间的同步与等待。
2. 任务包装与异步执行
2.1 std::async 与 std::future
std::async 可以在后台启动任务,并返回一个 std::future 供主线程获取结果。默认情况下,std::async 的策略是 launch::async 或 launch::deferred,可通过显式参数指定。
#include <future>
#include <numeric>
int main() {
auto fut = std::async(std::launch::async, std::accumulate, std::begin(nums), std::end(nums), 0);
// 继续做别的事
int sum = fut.get(); // 这里会等待结果
}
2.2 std::packaged_task 与 std::promise
这两者可以将函数包装成可被异步执行的任务,或者手动控制结果的设置与获取。
std::packaged_task<int(int, int)> task([](int a, int b){ return a + b; });
std::future <int> result = task.get_future();
std::thread(std::move(task), 2, 3).detach(); // 在新线程中执行
3. C++20 的 std::jthread:更安全的线程
std::jthread 在 std::thread 的基础上增加了自动停止功能,线程对象在析构时会尝试停止其执行。其构造函数接受一个 stop_token,可以让线程内部及时响应停止请求。
#include <jthread>
#include <iostream>
void worker(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Stopped\n";
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // 通知线程停止
}
std::jthread 在多线程程序中极大减少了资源泄露的风险,推荐在现代 C++ 代码中使用。
4. 协程(std::coroutine)的引入
4.1 协程基础概念
协程是一种 轻量级 的线程切换方式,函数执行可以被挂起(co_await、co_yield)并恢复,极大简化异步编程模型。
#include <coroutine>
#include <iostream>
struct Generator {
struct promise_type {
int current_value;
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() { return {}; }
void unhandled_exception() {}
void return_void() {}
};
struct iterator {
Generator* g;
bool operator!=(std::default_sentinel) { return true; }
int operator*() { return g->promise().current_value; }
iterator& operator++() { g->promise().yield_value(g->promise().current_value + 1); return *this; }
};
iterator begin() { return {this}; }
std::default_sentinel end() { return {}; }
};
4.2 实战示例:异步文件读取
在 C++20 之前,异步文件 I/O 通常需要回调或线程池。协程可以让异步读取像同步代码一样直观。
#include <filesystem>
#include <fstream>
#include <coroutine>
#include <string>
struct async_file_reader {
struct promise_type {
std::string data;
std::string filename;
async_file_reader get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { throw; }
void return_void() {}
void yield_value(std::string&& chunk) { data += std::move(chunk); }
};
struct coroutine_handle_t {
std::coroutine_handle <promise_type> h;
};
static coroutine_handle_t read(std::string path) {
auto co = async_file_reader{path};
// ...
}
};
(此处省略完整实现,重点是展示协程的使用方式)
5. 与第三方库的协作
5.1 ThreadPool(Boost.Asio)
Boost.Asio 提供了可跨平台的线程池与异步任务调度器。通过 io_context 可以轻松管理多个任务。
boost::asio::io_context io;
boost::asio::thread_pool pool(4);
boost::asio::post(pool, []{
// 任务内容
});
pool.join();
5.2 TBB(Threading Building Blocks)
TBB 的 parallel_for, parallel_reduce, task_group 等抽象可以让并行算法以声明式方式编写。
tbb::parallel_for(tbb::blocked_range <int>(0, N), [&](const tbb::blocked_range<int>& r){
for (int i=r.begin(); i!=r.end(); ++i) {
data[i] = heavy_computation(i);
}
});
6. 并发编程的最佳实践
- 避免数据竞争:使用
std::mutex、std::shared_mutex 或 std::atomic。
- 最小化锁粒度:只在必要时持有锁,减少阻塞。
- 使用 RAII 管理资源:
std::lock_guard、std::scoped_lock。
- 优先考虑线程池:减少频繁创建销毁线程的开销。
- 充分利用协程:降低线程开销,简化异步逻辑。
7. 小结
C++ 从最初的 std::thread 到现在的 std::jthread 与协程,已经形成了完整而强大的并发编程生态。通过结合标准库与成熟的第三方库,程序员可以在保持代码可读性和安全性的前提下,构建高效、可伸缩的并行应用。掌握这些工具与思想,能让你在面对大规模并发任务时游刃有余。