一、并发编程概述:为什么需要并发?

说实话,我第一次接触并发编程,是在一个后台服务项目里。那时候单线程跑得好好的,突然有一天用户量上来了,请求排队排到超时。我盯着监控面板,CPU 占用率才 20%,内存也绰绰有余,但响应时间就是下不来。嗯,这就是典型的「资源没吃满,活儿干不完」——说白了,我们需要并发。

为什么需要并发?

你想想看,一个程序如果只能一件事一件事地做,遇到 I/O 操作(读文件、发网络请求)就得干等着。CPU 在那边闲得发慌,磁盘或网卡却在慢慢悠悠地干活。这合理吗?当然不合理。

并发要解决的核心问题就两个:

  • 提高资源利用率——让 CPU 在等待 I/O 的时候去干别的活
  • 提升响应速度——多个任务同时推进,用户不用死等一个操作完成

我在项目中遇到过最典型的场景:一个图片处理服务,单线程处理一张图要 200ms,其中 150ms 都在等磁盘读写。改成多线程后,4 个线程同时干活,吞吐量直接翻了 3 倍多。你说香不香?

核心观点:并发不是为了让程序「跑得更快」,而是为了让程序「在单位时间内干更多的活」。这是两个概念,别搞混了。

二、进程与线程的区别

很多初学者会把进程和线程搞混。我刚开始学的时候也犯过这个错。其实区分它们很简单,记住一句话:进程是资源分配的最小单位,线程是 CPU 调度的最小单位。

进程

每个进程有自己的独立地址空间、堆栈、数据段。进程之间互相隔离,一个进程挂了,不影响其他进程。但代价是——创建和切换进程的开销很大。

  • 进程间通信(IPC)需要走管道、消息队列、共享内存等机制
  • 上下文切换要保存/恢复寄存器、内存映射、页表等
  • 创建进程需要复制父进程的资源(写时复制可以优化,但仍有开销)

线程

线程是进程里的「轻量级执行流」。同一进程下的线程共享地址空间和大部分资源,只有栈和寄存器是独立的。所以线程切换开销小得多,创建也快。

  • 线程间通信直接读写共享变量,方便但容易出问题
  • 一个线程崩溃,整个进程跟着挂(嗯,这是双刃剑)
  • 需要自己处理同步问题(互斥锁、条件变量等)
对比项 进程 线程
地址空间 独立 共享
切换开销
创建速度
通信方式 IPC 机制 共享内存
隔离性
适用场景 多任务、高隔离需求 高并发、低延迟

我的建议:如果你需要隔离性(比如浏览器每个标签页),用进程。如果你追求性能(比如 Web 服务器处理请求),用线程。但别滥用——线程不是越多越好,上下文切换也是有成本的。

三、C++ 并发编程的发展历程

C++ 的并发支持,说实话,起步挺晚的。早期全靠操作系统 API(pthread、Windows Thread),代码写起来又丑又容易出错。我早期维护过一个遗留系统,里面全是 pthread_create + 全局锁,看得我头皮发麻。

C++98/03 时代:裸奔

标准库没有线程支持。你想用多线程?自己调平台 API 吧。跨平台?自己封装吧。那时候的代码长这样:

// 伪代码,实际要处理一堆平台细节
#ifdef _WIN32
    CreateThread(NULL, 0, thread_func, NULL, 0, NULL);
#else
    pthread_create(&tid, NULL, thread_func, NULL);
#endif

嗯,每个项目都要自己写一套线程封装,而且 bug 频发。我见过最离谱的——有人忘了加锁,两个线程同时写同一个 vector,程序跑着跑着就崩了,还很难复现。

C++11:里程碑

C++11 终于把线程库纳入标准了。std::threadstd::mutexstd::lock_guard 这些基础组件都有了。跨平台终于不用自己造轮子了。

#include <thread>
#include <iostream>

void hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(hello);
    t.join();
    return 0;
}

这段代码现在看着简单,但在当时可是大新闻。我记得 C++11 刚发布那会儿,我兴奋地把项目里的 pthread 代码全换成了 std::thread,编译通过的那一刻,差点没哭出来。

C++14/17:完善与增强

C++14 主要是修修补补。C++17 带来了 std::scoped_lock(可以一次锁多个互斥量,避免死锁),还有并行算法(std::for_each 等可以指定执行策略)。

#include <algorithm>
#include <execution>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};
// 并行版 for_each
std::for_each(std::execution::par, v.begin(), v.end(), [](int& x) {
    x *= 2;
});

注意:并行算法不是银弹。数据量小的时候,并行调度的开销可能比串行还大。我曾经在一个只有几百个元素的数组上用了 std::execution::par,结果比单线程还慢——调度线程的开销盖过了并行带来的收益。

C++20:协程来了

C++20 引入了无栈协程(co_awaitco_yieldco_return)。协程不是线程,它是一种更轻量的并发原语——可以在函数内部挂起和恢复,而不需要操作系统参与调度。

#include <coroutine>
#include <future>

// 一个简单的协程示例(简化版)
std::future<int> async_task() {
    co_await std::async(std::launch::async, []{
        // 模拟耗时操作
        std::this_thread::sleep_for(1s);
    });
    co_return 42;
}

协程的好处是:你可以用同步的方式写异步代码,不用回调地狱。我个人觉得这是 C++ 并发编程未来的方向,但目前编译器支持还不够完善,生产环境用得还不多。

发展历程总结

从 C++98 到 C++20,并发支持越来越完善。我画了一张图,帮你理清脉络:

C++98 无标准线程支持 全靠平台API C++11 std::thread std::mutex std::future C++14 小修小补 shared_lock C++17 scoped_lock 并行算法 C++20 协程 co_await co_yield C++ 并发编程发展历程 从裸奔到协程,C++ 的并发支持越来越完善 无支持 基础支持 增强 协程时代

一句话总结:C++ 的并发支持从「自己造轮子」到「标准库原生支持」,再到「协程这种高级抽象」,走了将近 20 年。现在写并发代码,比十年前舒服太多了。

四、避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路:

  • 线程不是越多越好——我曾经在一个 4 核机器上开了 64 个线程处理数据,结果性能反而下降了。上下文切换的开销把并行收益全吃掉了。一般线程数不超过 CPU 核心数的 2 倍比较合理。
  • 别忽略数据竞争——两个线程同时写同一个变量,不加锁,结果就是未定义行为。我见过一个线上 bug,排查了三天才发现是少了个 mutex。
  • std::thread 必须 join 或 detach——否则析构时会调用 std::terminate,程序直接崩。嗯,这个坑我至少摔过两次。
  • 并行算法不是万能药——数据量小的时候,串行反而更快。我建议先测一下,别盲目上并行。

我的习惯:写并发代码之前,先画清楚数据流和同步点。别急着写代码,想清楚了再动手。并发 bug 最难排查,预防比事后修复重要得多。


公众号:蓝海资料掘金营,微信deep3321