21 · C++11/14/17/20 并发特性演进
说实话,每次标准更新,并发库都是重头戏。从 C++11 到 C++20,几乎每个版本都在给多线程开发“减负”。我最早用 C++03 写多线程,那会儿还得靠 pthread 或者 Windows API,移植性差不说,心智负担也重。后来 C++11 一出,std::thread 和 std::async 直接让我舒坦了不少。但版本一多,很多人就犯愁:到底该用哪个标准?怎么选技术栈?
这一章,我就按版本把并发特性拆开讲,再聊聊我自己的选型经验。你想想看,如果项目还在用 C++14,却硬上 C++20 的 std::jthread,那编译不过多尴尬。反过来,守着 C++11 不用 std::shared_mutex,又有点浪费。咱们得把每个版本的“杀手锏”摸清楚。
21.1 C++11:并发基础库的奠基
C++11 是真正的转折点。它第一次把线程、同步、异步任务纳入了标准库。我记得当时看到 std::thread 的时候,心里想的是:“终于不用再写平台相关的宏了。” 但说实话,C++11 的并发库还比较“原始”。比如 std::thread 必须手动 join 或 detach,否则析构时会 terminate。我在早期项目里就踩过这个坑——线程对象销毁了,但线程还在跑,程序直接崩掉。
std::thread—— 原生线程,支持移动语义,但必须管理生命周期。std::mutex/std::lock_guard—— 互斥锁与 RAII 封装。std::future/std::async—— 异步任务与结果获取。std::atomic—— 原子操作,无锁编程基础。std::condition_variable—— 条件变量,线程间通知。
我个人习惯,C++11 项目里尽量用 std::async 代替裸线程,因为它自动管理返回值和异常。但要注意,std::async 默认启动策略可能是同步或异步,依赖实现。如果你需要严格异步,记得传 std::launch::async。
21.2 C++14:小步快跑,完善读写锁
C++14 在并发方面改动不大,但补了一个重要角色:std::shared_timed_mutex 和 std::shared_lock。说白了,就是读写锁。读多写少的场景下,这东西能明显提升吞吐。我做过一个缓存服务,之前用 std::mutex 保护,并发一高就卡在锁上。换成 shared_mutex 后,读操作几乎不互斥,性能翻了一倍。
shared_timed_mutex 支持超时锁,但 C++17 的 shared_mutex 去掉了超时版本,性能更轻量。如果你不需要超时,优先用 C++17 的版本。
另外,C++14 还改进了 constexpr,让一些并发相关的常量表达式更灵活。不过对日常多线程开发影响不大,我就不展开了。
21.3 C++17:并行算法与更优雅的锁
C++17 是我个人最喜欢的一个版本。它带来了两样好东西:std::scoped_lock 和并行算法(PSTL)。先说 scoped_lock,它可以一次锁住多个 mutex,而且避免死锁(内部使用 std::lock)。我以前用 lock_guard 锁两个 mutex 时,总得小心翼翼保证顺序,稍不留神就死锁。有了 scoped_lock,代码干净多了。
// C++17 多锁示例
std::mutex m1, m2;
void safe_transfer() {
std::scoped_lock lock(m1, m2); // 同时锁住,避免死锁
// 操作共享数据...
}
并行算法更是让人眼前一亮。你只需要 #include <execution>,然后给 std::for_each 传一个执行策略 std::execution::par,就能自动并行。我在处理百万级数据排序时试过,四核机器上几乎线性加速。但要注意,并行算法要求迭代器是随机访问的,而且元素操作不能有数据竞争。
另外,C++17 正式把 std::shared_mutex 从 shared_timed_mutex 中独立出来,不带超时接口,性能更好。如果你只需要读写锁,直接用 std::shared_mutex 就对了。
21.4 C++20:协作中断与自动线程管理
C++20 在并发上迈了一大步。最吸引我的是 std::jthread —— 它会在析构时自动 join,再也不用担心忘记 join 导致程序崩溃了。而且它原生支持 std::stop_token,可以实现优雅的线程取消。以前我们想停止一个线程,得自己写一个原子标志位,现在标准库直接提供了。
// C++20 jthread + stop_token 示例
#include <thread>
#include <stop_token>
#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 << "worker stopped.\n";
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // 请求停止
// t 析构时自动 join
}
另一个亮点是 std::atomic_ref,它允许对非原子对象进行原子操作。比如你想临时对一个普通 int 变量做原子访问,又不想把它改成 std::atomic<int>,就可以用 atomic_ref。我在做零拷贝队列时用过这个,避免了修改原有数据结构。
此外,C++20 还增加了 std::counting_semaphore、std::latch 和 std::barrier。这些同步原语在并行循环、多阶段任务中非常实用。比如 std::latch 可以一次性等待多个线程到达某个点,而 std::barrier 可以重复使用。
21.5 如何选择合适的技术栈?
这个问题没有标准答案,但我可以分享几条经验。首先,看你的项目最低支持的 C++ 标准。如果还在用 C++11,那就老老实实用 std::thread + std::mutex,别硬上 C++20 的特性。如果可以用 C++17,我强烈建议上 scoped_lock 和并行算法,代码简洁且性能好。
- C++11 兼容项目: 用
std::async代替裸线程,用lock_guard管理锁,原子操作使用std::atomic。 - C++14 可用: 读多写少场景引入
shared_timed_mutex+shared_lock。 - C++17 首选: 多锁用
scoped_lock,数据并行用std::execution::par,读写锁用shared_mutex。 - C++20 新项目: 无脑上
jthread+stop_token,需要信号量用counting_semaphore,多阶段同步用barrier或latch。
另外,别忘了考虑编译器和平台支持。比如 MSVC 对 C++20 的 jthread 支持较早,但 GCC 直到 11 才完全实现。我一般会在 CI 里用最新编译器,同时保留一个 C++17 的 fallback 方案。
最后,我想说:技术栈不是越新越好。如果你的团队对 C++20 不熟悉,贸然引入 std::barrier 可能反而增加维护成本。我见过一个项目,为了用 jthread 把整个线程管理重构了,结果因为 stop_token 传递不当,出现了新的竞态条件。嗯,选型要结合团队和实际场景。