一、为什么需要并发?——从单核到多核的必然选择

先问大家一个问题:你写程序的时候,有没有遇到过「程序跑得太慢,CPU 占用率却只有 25%」的情况?

我遇到过。几年前做一个视频处理工具,单线程跑,CPU 四核只用了 25%。说白了,其他三个核都在闲着。用户等得着急,我也着急。

这就是并发最直接的价值——榨干硬件性能

1.1 并发能解决什么问题?

  • 提高吞吐量:同样的时间,做更多的事。比如服务器同时处理 1000 个请求。
  • 降低延迟:用户点个按钮,不用等前一个任务完成才响应。
  • 充分利用多核:现在的手机都是 8 核起步,你只用 1 个核,是不是有点浪费?
  • 更好的用户体验:UI 不卡顿,后台默默干活。

核心观点:并发不是「锦上添花」,而是「不得不做」。尤其是服务端、游戏引擎、音视频处理这些领域,没有并发,基本没法用。

1.2 并发 ≠ 并行

这两个词经常被混用,但区别其实挺大。

概念含义例子
并发(Concurrency)多个任务在逻辑上同时执行单核 CPU 快速切换任务,看起来像同时跑
并行(Parallelism)多个任务在物理上同时执行多核 CPU,每个核跑一个任务

说白了,并发是「看起来同时做」,并行是「真的同时做」。C++ 的线程库两者都支持,但底层原理不同。

二、进程与线程的区别——别再搞混了

我记得刚学操作系统那会儿,老师问:「进程和线程有什么区别?」全班沉默。后来自己写代码踩了坑,才真正理解。

2.1 进程(Process)

  • 资源分配的最小单位:每个进程有独立的地址空间、文件描述符、信号处理等。
  • 开销大:创建进程需要复制一大堆资源,切换成本也高。
  • 隔离性强:一个进程挂了,不影响其他进程。

2.2 线程(Thread)

  • CPU 调度的最小单位:线程共享进程的地址空间和资源。
  • 开销小:创建线程只需要分配栈空间,切换成本低。
  • 隔离性弱:一个线程崩溃,整个进程可能挂掉。

我的经验:项目中如果任务之间需要大量共享数据,用线程更合适。如果任务需要强隔离(比如不同用户的服务),用进程更安全。

2.3 一张图看懂关系

进程(Process) 地址空间 | 文件描述符 | 信号处理 | 环境变量 独立资源,开销大,隔离性强 线程 1 栈 | 寄存器 共享进程资源 线程 2 栈 | 寄存器 共享进程资源 线程 3 栈 | 寄存器 共享进程资源 一个进程包含多个线程,线程共享进程资源

三、C++11 线程库概述——从零到一

C++11 之前,想用线程?你得用 POSIX threads(pthreads)或者 Windows API。不同平台写法不一样,移植起来想哭。

C++11 终于把线程支持加进了标准库。嗯,虽然晚了点,但总比没有好。

3.1 核心组件一览

头文件类/函数作用
<thread>std::thread创建和管理线程
<mutex>std::mutex互斥锁,保护共享数据
<future>std::future / std::promise异步获取结果
<atomic>std::atomic原子操作,无锁编程基础
<condition_variable>std::condition_variable线程间条件等待

注意:C++11 的线程库是跨平台的。你在 Windows 上写的代码,拿到 Linux 上编译就能跑。这一点,我当年用 pthreads 的时候想都不敢想。

四、第一个线程程序——Hello, Thread!

光说不练假把式。咱们直接写代码。

4.1 最简单的例子

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread! ID: " 
              << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(hello);  // 创建线程,执行 hello 函数
    t.join();              // 等待线程结束
    std::cout << "Hello from main!" << std::endl;
    return 0;
}

编译命令(g++):

g++ -std=c++11 -pthread -o hello hello.cpp

避坑指南:我曾经忘记加 -pthread 链接选项,结果编译通过,运行时报错。嗯,Linux 下必须加这个选项,否则链接不到 pthread 库。

4.2 代码解释

  • std::thread t(hello):创建一个线程,入口函数是 hello()
  • t.join():主线程等待 t 线程执行完毕。如果不调用 join()detach(),程序会崩溃。
  • std::this_thread::get_id():获取当前线程的唯一 ID。

重要std::thread 对象销毁前,必须调用 join()detach()。否则程序会调用 std::terminate() 直接终止。我见过不少新手在这里翻车。

4.3 带参数的线程函数

#include <iostream>
#include <thread>

void print_sum(int a, int b) {
    std::cout << "Sum: " << (a + b) << std::endl;
}

int main() {
    std::thread t(print_sum, 3, 4);  // 传递参数
    t.join();
    return 0;
}

参数会按值传递。如果需要传引用,得用 std::ref() 包装。这个后面再细讲。

4.4 线程的执行流程

主线程 创建线程 子线程 执行任务 等待 join() 子线程结束,主线程继续执行

五、常见陷阱与最佳实践

5.1 陷阱一:忘记 join 或 detach

前面说过,std::thread 析构时如果线程还在运行,程序会终止。解决办法:

  • 确保所有分支都调用了 join()detach()
  • 使用 RAII 包装(后面章节会讲)

5.2 陷阱二:线程函数抛出异常

如果线程函数内部抛异常且没被捕获,程序会调用 std::terminate()。建议在线程函数内用 try-catch 包一层。

5.3 陷阱三:访问已销毁的变量

// 错误示例
void func(int& x) {
    // 如果 x 已经销毁,这里就出事了
}

int main() {
    int a = 42;
    std::thread t(func, std::ref(a));
    // a 可能先被销毁
    t.detach();
    return 0;
}

我曾经在项目里犯过这个错。一个线程访问了主线程的局部变量,主线程退出后变量销毁,子线程直接崩溃。排查了好久才发现。

六、总结

这一章我们聊了:

  • 为什么需要并发——说白了就是让程序跑得更快、响应更及时
  • 进程和线程的区别——进程是资源容器,线程是执行单元
  • C++11 线程库的核心组件——thread、mutex、future、atomic 等
  • 第一个线程程序——创建、等待、传参
  • 常见陷阱——join/detach、异常、变量生命周期

并发编程不是一蹴而就的事。我刚开始写多线程代码时,也经常遇到死锁、数据竞争这些问题。别怕,后面我们会一个一个攻克。

记住:先跑起来,再优化。别一开始就想着写完美的并发代码,那是不可能的。

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