一、并发、并行和异步有什么区别
1. 并发(Concurrency)
并发指的是:
多个任务在同一时间段内交替推进。
假设 CPU 只有一个核心,需要处理三个任务:
时间 ─────────────────────────→
CPU核心:
任务A → 任务B → 任务C → 任务A → 任务B → 任务C
虽然 CPU 同一时刻只能执行一个任务,但由于切换速度很快,看起来像多个任务同时运行。
这就是并发。
例如:
void taskA() { /* 处理任务A */ }
void taskB() { /* 处理任务B */ }
我们可以创建两个线程:
std::thread t1(taskA); // 创建线程1
std::thread t2(taskB); // 创建线程2
t1.join(); // 等待线程1结束
t2.join(); // 等待线程2结束
这两个线程可以并发执行。
但注意:
并发不代表两个线程一定在同一时刻运行。
2. 并行(Parallelism)
并行指的是:
多个任务在同一时刻真正执行。
例如有两个 CPU 核心:
时间 ─────────────────────→
CPU核心1:任务A ───────────→
CPU核心2:任务B ───────────→
此时:
任务A和任务B
↓
真正同时执行
因此:
-
并发:强调多个任务能够交替推进。
-
并行:强调多个任务真正同时执行。
并行通常需要多个可用的 CPU 执行核心,而并发不一定需要。
3. 异步(Asynchronous)
异步关注的不是有多少线程,而是:
发起一个操作后,不必立即等待它完成,可以先执行其他工作。
例如同步执行:
downloadFile(); // 等待下载完成
processFile(); // 下载结束后才能执行
执行顺序:
开始下载
↓
一直等待
↓
下载完成
↓
处理文件
如果采用异步方式:
发起下载
↓
下载任务继续进行
↓
主流程先执行其他工作
↓
下载完成
↓
获取结果并处理
核心区别:
同步
↓
当前流程需要等待结果才能继续
异步
↓
当前流程可以先做别的事情
之后再获取结果
注意,异步不一定需要创建新线程。
例如 Linux 的 epoll 配合非阻塞 socket,也可以在单线程事件循环中组织多个 I/O 操作,不需要为每个连接创建一个线程。
二、使用std::thread实现多线程并发
C++11 提供了:
#include <thread>
可以直接创建线程。
例如:
#include <iostream>
#include <thread>
void task(int id) { std::cout << "任务:" << id << '\n'; } // 线程任务函数
int main() {
std::thread t1(task, 1); // 创建线程1,执行task(1)
std::thread t2(task, 2); // 创建线程2,执行task(2)
t1.join(); // 等待线程1执行结束
t2.join(); // 等待线程2执行结束
return 0;
}
执行流程:
主线程
│
┌───────┴───────┐
↓ ↓
线程t1 线程t2
↓ ↓
task(1) task(2)
│ │
└───────┬───────┘
↓
主线程结束
这里有两个重要函数。
join():等待线程结束
t1.join(); // 当前线程等待t1执行完成
如果 t1 还没执行完:
调用join()
↓
当前线程阻塞等待
↓
t1执行结束
↓
当前线程继续
detach():分离线程
t1.detach(); // 线程独立运行,不再由t1对象管理等待
分离以后,不能再对 t1 调用 join()。
需要特别注意:detach() 并不代表线程绝对安全,它仍然可能访问已经销毁的对象,因此实际开发中应谨慎使用。
还有一个常见面试陷阱:
std::thread t1(task, 1);
t1.join();
std::thread t2(task, 2);
t2.join();
虽然创建了两个线程,但它们是依次执行的。
因为:
t1启动
↓
等待t1完成
↓
t2启动
↓
等待t2完成
所以想让两个任务有机会并发运行,应该先启动两个线程,再等待它们完成。
三、使用std::async实现异步任务
C++11 还提供了:
#include <future>
其中两个核心工具是:
std::async // 启动异步任务
std::future // 获取任务结果
例如:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int calculate() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
return 100; // 返回计算结果
}
int main() {
auto result = std::async(std::launch::async, calculate); // 启动异步任务
std::cout << "主线程继续执行其他工作\n"; // 不必立即等待
std::cout << "计算结果:" << result.get() << '\n'; // 获取结果,未完成则阻塞
return 0;
}
执行流程:
主线程
│
├── std::async()
│ ↓
│ 异步执行calculate()
│ ↓
│ 计算完成,保存结果
│
├── 主线程继续工作
│
└── future.get()
↓
获取计算结果
这里最重要的是:
std::async(std::launch::async, calculate);
其中:
std::launch::async
明确要求任务异步执行,而不是等到获取结果时才开始执行。
如果不指定启动策略:
auto result = std::async(calculate);
实现可以选择立即异步执行,也可以选择延迟执行。
另外:
result.get();
并不是永远不阻塞。
如果异步任务还没有完成:
future.get()
↓
等待结果
↓
任务完成
↓
返回结果
所以异步真正的优势是:
在启动任务和获取结果之间,主线程可以执行其他工作。
例如:
auto result = std::async(std::launch::async, calculate); // 启动异步计算
doOtherWork(); // 主线程处理其他任务
int value = result.get(); // 需要结果时再等待
还有一个重要区别:
std::thread 通常需要自己处理线程的生命周期和任务结果,而 std::async 可以通过 std::future 获取返回值以及任务中的异常。
需要注意的是,std::async(std::launch::async, ...) 返回的 future 在特定销毁场景下也可能等待任务完成。因此,不能简单认为只要使用 std::async,当前线程就绝对不会阻塞。
四、线程并发与异步有哪些关系
这是面试中最容易混淆的部分。
其实:
多线程是一种执行机制,异步是一种任务组织方式。
两者不是互相对立的概念。
例如:
多线程 + 同步等待
std::thread t(task); // 启动工作线程
t.join(); // 立即等待结果
这里使用了多线程,但当前线程立即进入同步等待。
再比如:
多线程 + 异步执行
std::thread t(task); // 启动任务
doOtherWork(); // 主线程继续工作
t.join(); // 最后再等待
主线程没有立即等待,而是先执行了其他操作。
还有:
单线程 + 异步I/O
例如 Linux 网络服务器:
epoll事件循环
│
┌────────┼────────┐
↓ ↓ ↓
fd1 fd2 fd3
│ │ │
└────────┼────────┘
↓
非阻塞读写I/O
整个程序即使只有一个事件线程,也可以管理大量连接。
因为它不是:
读取fd1
↓
阻塞等待数据
↓
处理fd2
而是:
等待就绪事件
↓
fd1可读 → 处理fd1
fd2可读 → 处理fd2
fd3可写 → 处理fd3
所以:
并发不一定需要多核
异步不一定需要多线程
多线程也不代表一定异步
多线程并发还需要注意数据竞争
例如:
int count = 0; // 共享变量
void add() {
for (int i = 0; i < 10000; ++i) ++count; // 多线程修改,存在数据竞争
}
如果两个线程同时执行:
std::thread t1(add); // 线程1修改count
std::thread t2(add); // 线程2修改count
t1.join(); // 等待线程1
t2.join(); // 等待线程2
那么:
++count;
不是一个普通意义上不可分割的原子操作。
可能出现:
线程1读取count = 10
线程2读取count = 10
线程1计算11
线程2计算11
线程1写回11
线程2写回11
本来应该增加两次:
10 → 12
结果却丢失了一次更新。
在 C++ 中,无同步地并发读写同一非原子变量属于数据竞争,会导致未定义行为。
解决办法之一:
#include <atomic>
std::atomic<int> count{0}; // 原子变量
void add() {
for (int i = 0; i < 10000; ++i) ++count; // 原子递增
}
如果是多个变量组成的复杂共享状态,则通常需要:
std::mutex
std::lock_guard
来保护临界区。
因此并发开发中除了追求性能,还必须关注:
线程安全
锁竞争
死锁
数据竞争
任务调度
五、实际开发中应该如何选择
常见方法对比:
| 技术 | 核心特点 | 适用场景 |
|---|---|---|
std::thread |
显式创建和管理线程 | 独立线程任务、长期运行的工作线程 |
std::async |
启动任务并通过 future 获取结果 | 简单后台计算、有返回值的异步任务 |
| 线程池 | 复用工作线程 | 大量需要调度的中小型任务 |
epoll |
I/O 多路复用 | Linux 高并发网络服务器 |
| Qt 信号槽与工作线程 | 跨线程通知和事件驱动 | Qt 客户端、GUI 后台任务 |
例如 Qt 客户端开发:
GUI主线程
↓
点击"开始分析"
↓
工作线程解析大型日志文件
↓
分析完成
↓
发送信号
↓
GUI主线程更新界面
为什么要这样设计?
因为如果直接在 GUI 主线程执行:
读取大型文件
复杂数据计算
数据库查询
就可能阻塞 GUI 事件循环,导致界面卡顿。
而使用工作线程:
耗时任务在后台运行
↓
主线程继续响应鼠标、键盘
↓
处理结束后通知界面
用户体验就会更好。
不过需要注意,Qt 的 GUI 控件通常只能在 GUI 线程操作,因此工作线程不应该直接修改界面控件。
面试怎么回答
问题1:并发和并行有什么区别?
并发是多个任务在同一时间段内交替推进,可以通过线程调度实现;并行是多个任务在同一时刻真正运行,通常需要多个 CPU 核心。并发强调任务组织和推进,并行强调物理上的同时执行。
问题2:多线程和异步有什么区别?
多线程是通过多个线程执行任务,异步是任务启动后不必立即等待结果,可以继续处理其他事情。异步可以通过工作线程实现,也可以通过单线程事件循环和非阻塞 I/O 实现,因此异步不等于多线程。
问题3:std::thread 和 std::async 有什么区别?
std::thread主要负责创建和管理线程,用户需要通过join或detach管理线程状态;std::async更关注异步任务,可以通过std::future获取结果及异常。但要注意指定启动策略,否则任务可能延迟执行。
问题4:异步一定比同步快吗?
不一定。异步的主要价值是避免当前流程在等待期间无事可做,提高任务之间的重叠程度和程序响应能力。如果任务本身很短,异步调度开销反而可能超过收益。对于 CPU 密集型任务,还要考虑 CPU 核心数和线程竞争情况。
问题5:什么情况使用线程池?
如果系统中存在大量短任务,频繁创建和销毁线程会产生额外开销。可以提前创建一定数量的工作线程,通过任务队列分发任务,从而复用线程资源,并控制并发数量。
最后将整个知识体系总结为:
任务执行方式
│
┌─────────────┼──────────────┐
↓ ↓ ↓
并发 并行 异步
↓ ↓ ↓
多任务交替推进 多任务同时执行 发起后不立即等待
│ │ │
↓ ↓ ↓
线程调度 多CPU核心 future/事件回调
│
┌─────────┴─────────┐
↓ ↓
多线程实现 单线程事件循环
↓ ↓
std::async epoll
可以记住以下四句话:
-
并发不等于并行:并发关注多个任务的推进,并行关注真正同时执行。
-
异步不等于多线程:单线程事件循环也能实现异步处理。
-
多线程不一定更快:线程创建、上下文切换和锁竞争都会带来开销。
-
异步不代表永不阻塞 :调用
future.get()、join()或其他等待操作时,当前线程仍然可能被阻塞。
对于 C++ 开发而言,真正重要的是根据任务特点选择合理的执行模型,而不是单纯追求更多线程或者更多异步接口。