一、为什么需要并发?——从单核到多核的必然选择
先问大家一个问题:你写程序的时候,有没有遇到过「程序跑得太慢,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 一张图看懂关系
三、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 线程的执行流程
五、常见陷阱与最佳实践
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