一文理解后端核心概念:同步/异步、阻塞/非阻塞、进程/线程/协程
作为开发者,你是否也曾被这些问题困扰?
"同步和异步到底有啥区别?"
"阻塞和非阻塞是一回事吗?"
"进程、线程、协程的关系怎么理清?"
"并发和并行能划等号吗?"
这些概念看似零散,实则归属于不同维度与层次。本文尝试用通俗的语言、形象的类比和可运行的代码,系统梳理这些核心概念,帮助你理解其内在逻辑。
一、先看全局:核心概念关系图
首先用一张图理清所有概念的层级关系:

简单总结:应用程序由进程构成,进程包含线程,线程内可调度协程;而同步/异步描述执行顺序,阻塞/非阻塞描述等待状态,并发/并行描述任务关系。
二、同步 vs 异步:关注"执行顺序"
核心区别:是否需要等待当前任务完成才能执行下一个

同步(Synchronous)
- 任务按顺序执行,必须等当前任务完成,才能启动下一个
- 调用者需要等待结果返回,期间无法做其他事
异步(Asynchronous)
- 任务可独立执行,无需等待当前任务完成
- 调用者发起操作后立即返回,结果通过回调、Future等方式通知
生活类比
- 同步:去餐厅点餐 → 等厨师做饭 → 拿到饭 → 开始吃(全程等待,啥也干不了)
- 异步:点外卖 → 立即做其他事 → 外卖到了通知你 → 去拿外卖(无需等待,可正常工作)
C++代码示例
同步读取文件
cpp
#include <iostream>
#include <fstream>
#include <string>
void synchronous_read() {
std::cout << "开始读取文件..." << std::endl;
std::ifstream file("data.txt");
std::string content;
// 此处会阻塞,直到读取完成
if (file.is_open()) {
std::getline(file, content, '\0');
file.close();
}
std::cout << "文件读取完成: " << content << std::endl;
std::cout << "继续执行其他操作" << std::endl;
}
异步读取文件(C++20协程)
cpp
#include <iostream>
#include <coroutine>
#include <thread>
#include <future>
// 简化的异步文件读取
std::future<std::string> async_read_file(const std::string& filename) {
return std::async(std::launch::async, [filename]() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟IO耗时
return "文件内容: Hello World";
});
}
void asynchronous_read() {
std::cout << "开始异步读取文件..." << std::endl;
// 发起异步操作,立即返回
auto future = async_read_file("data.txt");
std::cout << "可以继续做其他事情!" << std::endl;
std::cout << "做一些其他工作..." << std::endl;
// 需要结果时再获取
std::string content = future.get();
std::cout << "异步读取完成: " << content << std::endl;
}
三、阻塞 vs 非阻塞:关注"等待状态"
核心区别:等待结果时,线程是否会挂起并失去CPU控制权

阻塞(Blocking)
- 调用后,线程会挂起等待,失去CPU控制权
- 直到操作完成或超时才返回,期间无法执行其他任务
非阻塞(Non-blocking)
- 调用后立即返回结果(可能是"尚未完成"的状态)
- 线程保持活跃,可执行其他任务,需通过轮询或事件通知获取最终结果
生活类比
- 阻塞:去银行办业务 → 排队 → 叫号 → 站在窗口前等待办理(全程不能离开,啥也干不了)
- 非阻塞:取号后 → 去旁边玩手机 → 时不时看叫号屏幕 → 叫到号再去办理(可做其他事,无需傻等)
C++代码示例
阻塞IO(Socket读取)
cpp
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
void blocking_read() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
// ... 连接服务器代码省略 ...
char buffer[1024];
std::cout << "等待接收数据(阻塞中)..." << std::endl;
// recv是阻塞调用,会一直等待直到有数据到达
ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0);
std::cout << "收到数据: " << bytes << " 字节" << std::endl;
// 只有收到数据后,才会执行到这里
close(sock);
}
非阻塞IO(Socket读取)
cpp
#include <iostream>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
void non_blocking_read() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置为非阻塞模式
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
// ... 连接服务器代码省略 ...
char buffer[1024];
while (true) {
// recv立即返回,不会等待
ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0);
if (bytes > 0) {
std::cout << "收到数据: " << bytes << " 字节" << std::endl;
break;
} else if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 暂时没有数据,可以做其他事
std::cout << "暂无数据,做点其他事..." << std::endl;
sleep(1); // 模拟其他工作
} else {
// 真正的错误
break;
}
}
close(sock);
}
四、关键区分:同步/异步 与 阻塞/非阻塞
这是最容易混淆的两对概念!记住核心:
- 同步/异步:关注任务执行顺序(消息通知机制)
- 阻塞/非阻塞:关注线程等待状态
它们可以组合出5种主要场景,对应不同的使用场景:
| 组合 | 特点 | 使用场景 | 例子 |
|---|---|---|---|
| 同步阻塞 | 发起调用后阻塞等待完成 | 简单场景,逻辑清晰 | 普通的read/write |
| 同步非阻塞(轮询) | 立即返回,需主动重试 | 需要持续检查状态 | O_NONBLOCK + 轮询 |
| 同步非阻塞(多路复用) | 阻塞等待多个FD就绪 | 高并发服务器 | select/poll/epoll |
| 异步阻塞 | 发起异步操作但阻塞等待 | 罕见,效率低 | aio_read + aio_suspend |
| 异步非阻塞 | 发起后立即返回,完成后回调 | 极致性能 | io_uring, Linux AIO, IOCP |
重点澄清:epoll不是异步IO!
很多人误以为epoll是"异步非阻塞",这是常见误解!
根据POSIX标准定义:
- 同步I/O:导致请求进程阻塞,直到I/O操作完成
- 异步I/O:不导致请求进程阻塞
epoll的本质是同步I/O多路复用------用一个阻塞调用同时监听多个文件描述符。虽然监听的socket是非阻塞的,但epoll_wait()本身会阻塞等待事件,且最终的数据拷贝操作(如recvfrom)仍然会阻塞进程。
真正的异步非阻塞IO示例(io_uring)
cpp
#include <iostream>
#include <liburing.h>
// 真正的异步非阻塞IO: io_uring
void async_io_uring_example() {
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
char buffer[1024];
int fd = open("test.txt", O_RDONLY);
// 1. 提交异步读请求(立即返回,不阻塞)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
io_uring_sqe_set_data(sqe, buffer);
io_uring_submit(&ring);
std::cout << "异步读请求已提交,立即返回,可以做其他事..." << std::endl;
std::cout << "做其他工作..." << std::endl;
// 2. 等待完成(此处可选择阻塞或非阻塞等待)
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 3. 处理结果
if (cqe->res >= 0) {
std::cout << "异步读取完成: " << cqe->res << " 字节" << std::endl;
}
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
close(fd);
}
五、进程、线程、协程:资源粒度递减
这三个概念描述的是任务执行的资源分配单位,核心区别在于资源占用和调度开销:
| 特性 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
|---|---|---|---|
| 创建时间 | 毫秒(ms) | 微秒(μs) | 纳秒(ns) |
| 内存占用 | 数MB | ~1MB栈 | 几十字节 |
| 切换开销 | 大 | 中 | 极小 |
| 通信方式 | IPC(管道/共享内存) | 直接共享内存 | 直接访问 |
| 并行能力 | 真并行 | 真并行 | 并发(非并行) |
核心特点
- 进程:最重量级,有独立内存空间,隔离性强,一个进程崩溃不影响其他进程
- 线程:中等重量,共享进程内存,一个线程崩溃可能导致整个进程崩溃
- 协程:最轻量级,在线程内调度,协作式调度(需要主动让出CPU)
C++代码示例
进程示例
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
void process_example() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
std::cout << "子进程ID: " << getpid() << std::endl;
sleep(2);
exit(0);
} else {
// 父进程
std::cout << "父进程ID: " << getpid()
<< ", 子进程ID: " << pid << std::endl;
wait(nullptr); // 等待子进程结束
}
}
线程示例
cpp
#include <iostream>
#include <thread>
#include <vector>
void worker(int id) {
std::cout << "线程 " << id << " 开始工作" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "线程 " << id << " 完成工作" << std::endl;
}
void thread_example() {
std::vector<std::thread> threads;
// 创建5个线程
for (int i = 0; i < 5; i++) {
threads.emplace_back(worker, i);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
}
协程示例(C++20)
cpp
#include <iostream>
#include <coroutine>
#include <thread>
// 简单的协程返回类型
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task coroutine_worker(int id) {
std::cout << "协程 " << id << " 开始工作" << std::endl;
// 模拟异步操作,主动让出CPU
co_await std::suspend_always{};
std::cout << "协程 " << id << " 恢复工作" << std::endl;
}
void coroutine_example() {
// 轻松创建10万个协程,换成线程内存早溢出了!
for (int i = 0; i < 100000; i++) {
coroutine_worker(i);
}
}
六、并发 vs 并行:任务的两种执行方式
核心区别:是否真正同时执行

并发(Concurrency)
- 多个任务交替执行,看起来同时进行,实际是快速切换
- 单个CPU即可实现(如单线程事件循环)
并行(Parallelism)
- 多个任务真正同时执行
- 需要多个CPU核心支持
生活类比
- 并发:一个厨师做多道菜 → 炒一会儿菜1,再炒一会儿菜2,快速切换(单核CPU)
- 并行:多个厨师同时做菜 → 厨师1炒莱1,厨师2炒莱2(多核CPU)
C++代码示例
并发示例(单线程事件循环)
cpp
#include <iostream>
#include <queue>
#include <functional>
// 简单的事件循环(并发但不并行)
class EventLoop {
private:
std::queue<std::function<void()>> tasks;
public:
void add_task(std::function<void()> task) {
tasks.push(task);
}
void run() {
while (!tasks.empty()) {
auto task = tasks.front();
tasks.pop();
// 执行任务(单线程,交替执行)
task();
}
}
};
void concurrent_example() {
EventLoop loop;
// 添加多个任务
loop.add_task([]() {
std::cout << "任务1执行" << std::endl;
});
loop.add_task([]() {
std::cout << "任务2执行" << std::endl;
});
loop.add_task([]() {
std::cout << "任务3执行" << std::endl;
});
// 并发执行(快速切换)
loop.run();
}
并行示例(多线程)
cpp
#include <iostream>
#include <thread>
#include <vector>
void parallel_worker(int id) {
std::cout << "并行任务 " << id
<< " 在线程 " << std::this_thread::get_id()
<< " 执行" << std::endl;
}
void parallel_example() {
std::vector<std::thread> threads;
// 创建多个线程,真正并行执行
for (int i = 0; i < 4; i++) {
threads.emplace_back(parallel_worker, i);
}
for (auto& t : threads) {
t.join();
}
}
七、异步编程的核心:回调与事件循环
1. 回调(Callback)
回调是异步操作完成后,用于通知调用者的函数,是异步编程的基础。

生活类比
点外卖时留电话:你下单 → 留电话(回调函数)→ 做其他事 → 外卖到了(调用回调)→ 取外卖
C++代码示例
cpp
#include <iostream>
#include <functional>
#include <thread>
#include <chrono>
// 异步下载函数,接受回调
void async_download(
const std::string& url,
std::function<void(const std::string&)> callback) {
// 创建线程模拟异步下载
std::thread([url, callback]() {
std::cout << "开始下载: " << url << std::endl;
// 模拟下载耗时
std::this_thread::sleep_for(std::chrono::seconds(2));
// 下载完成,调用回调
callback("下载完成的数据");
}).detach();
}
void callback_example() {
std::cout << "主线程:发起下载请求" << std::endl;
// 发起异步下载,传入回调函数
async_download("http://example.com/file",
[](const std::string& data) {
std::cout << "回调执行: " << data << std::endl;
}
);
std::cout << "主线程:继续做其他事情" << std::endl;
// 等待异步操作完成
std::this_thread::sleep_for(std::chrono::seconds(3));
}
2. 事件循环(Event Loop)
事件循环是不断检查并处理事件的无限循环,是单线程异步系统(如Node.js、浏览器)的核心。
工作原理

C++实现简单事件循环
cpp
#include <iostream>
#include <queue>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
class SimpleEventLoop {
private:
std::queue<std::function<void()>> event_queue;
std::mutex mtx;
std::condition_variable cv;
bool running = true;
public:
// 添加事件到队列
void post_event(std::function<void()> event) {
std::lock_guard<std::mutex> lock(mtx);
event_queue.push(event);
cv.notify_one();
}
// 事件循环主函数
void run() {
while (running) {
std::unique_lock<std::mutex> lock(mtx);
// 等待事件
cv.wait(lock, [this]() {
return !event_queue.empty() || !running;
});
if (!running) break;
// 取出事件
auto event = event_queue.front();
event_queue.pop();
lock.unlock();
// 执行事件处理
event();
}
}
void stop() {
running = false;
cv.notify_all();
}
};
void event_loop_example() {
SimpleEventLoop loop;
// 在另一个线程运行事件循环
std::thread loop_thread([&loop]() {
loop.run();
});
// 向事件循环投递事件
loop.post_event([]() {
std::cout << "事件1: 处理网络请求" << std::endl;
});
loop.post_event([]() {
std::cout << "事件2: 处理文件IO" << std::endl;
});
loop.post_event([]() {
std::cout << "事件3: 更新UI" << std::endl;
});
// 等待事件处理
std::this_thread::sleep_for(std::chrono::seconds(2));
loop.stop();
loop_thread.join();
}
八、实战指南:如何选择合适的方案?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| CPU密集型任务 | 多进程/多线程 | 需要真正的并行计算,充分利用多核CPU |
| IO密集型任务(高并发) | IO多路复用(epoll)+协程 | 单线程处理大量连接,减少线程切换开销 |
| IO密集型任务(极致性能) | io_uring(真异步) | 零拷贝,内核异步处理,性能最优 |
| 简单脚本工具 | 同步阻塞 | 代码简单,开发效率高,够用即可 |
| 高性能服务器 | 事件循环+IO多路复用+线程池 | 结合多种技术,平衡性能和开发复杂度 |
| Web后端 | 协程+IO多路复用 | 如Go、Python asyncio方案,兼顾高并发和开发效率 |
最佳实践
- 优先使用同步阻塞,除非有明确的性能瓶颈
- IO密集型用IO多路复用,CPU密集型用多线程/进程
- 百万级连接场景用协程,资源占用少、切换快
- 主流高性能方案:事件循环+epoll(Nginx、Redis、Node.js均采用)
- 极致性能需求用io_uring(Linux 5.1+支持)
九、常见问题
-
同步一定是阻塞的吗?
不一定!同步非阻塞(如轮询)和IO多路复用都是同步但非阻塞的反例。
-
epoll是异步IO吗?
不是!epoll是同步IO多路复用,epoll_wait()会阻塞等待事件,真正的异步IO是io_uring、Linux AIO。
-
协程和线程的区别?
协程是用户态调度,轻量级(纳秒级创建);线程是内核态调度,重量级(微秒级创建)。
-
单核CPU能实现并行吗?
不能!单核只能实现并发(快速切换任务),并行需要多核CPU支持。
十、总结
其实这些概念并不复杂,关键是抓住核心维度:
- 同步/异步 → 执行顺序
- 阻塞/非阻塞 → 等待状态
- 进程/线程/协程 → 资源粒度
- 并发/并行 → 任务关系
- 回调/事件循环 → 实现机制
