前言
在深入理解协程(coroutine)和异步编程(asynchronous programming)之前,我们需要先搞清楚几个基础概念:并发 vs 并行、I/O-bound vs CPU-bound、同步/异步 & 阻塞/非阻塞。这些概念是理解现代编程模型的基石。
并发 vs 并行
并发(Concurrency)和并行(Parallelism)是两个经常被混用的概念,但它们关注的重点不同:
并发(Concurrency)
定义:在同一时间段内,系统能处理多个任务,但不一定是同时进行的,而是通过切换来让人感觉“同时”在进行。
比喻:一个人吃饭的时候,接到电话,他可以放下筷子去接电话,然后继续吃饭,看起来两个任务都在进行,但实际上是交替执行。
特点:
- 关注任务的组织和调度。
- 强调“同时应对很多事情”,而不是同时完成。
- 典型实现方式是时间片轮转(操作系统在单核 CPU 上调度多个线程)。
并行(Parallelism)
定义:在同一时刻,多个任务真正地在多个处理器或内核上同时执行。
比喻:两个人,一个吃饭,一个接电话,两件事同时发生。
特点:
- 需要多核 CPU 或分布式系统的支持。
- 强调“同时做很多事情”。
- 更依赖硬件并发度。
区别总结
维度 | 并发 (Concurrency) | 并行 (Parallelism) |
---|---|---|
本质 | 逻辑上的同时执行(交替调度) | 物理上的同时执行 |
强调点 | 任务的调度与协调 | 任务的同时执行 |
硬件依赖 | 单核也能实现 | 需要多核或多机 |
举例 | 多个用户程序在单核上切换运行 | 多核 CPU 同时执行多个线程 |
可以这样记:
- 并发 = 会“同时应付”多个任务(不一定同时做)
- 并行 = 会“同时进行”多个任务(真正在同时做)
I/O-bound vs CPU-bound
1. I/O-bound(I/O 受限任务)
定义:程序的主要瓶颈在 等待 I/O(输入/输出),而不是计算本身。
典型场景:
- 读写磁盘文件(数据库查询、日志写入)
- 网络请求(HTTP 调用、Socket 通信)
- 用户输入/输出(键盘、屏幕、打印机)
特点:
- CPU 大部分时间 在等待 外设返回结果。
- 任务的性能瓶颈取决于 I/O 速度。
- 适合用 异步 I/O 或 多线程 来提升并发度,让 CPU 不至于闲着。
举例:下载 100 张图片。CPU 处理单张图片的耗时很短,但大部分时间都在等网络返回,这就是 I/O-bound。
2. CPU-bound(CPU 受限任务)
定义:程序的主要瓶颈在 计算处理,而不是 I/O。
典型场景:
- 图像/视频处理(压缩、解码、滤镜)
- 机器学习模型训练/推理
- 加密/解密、大量数学运算
特点:
- CPU 一直在满负荷运转。
- 任务的性能瓶颈取决于 CPU 计算能力。
- 优化方式通常是 并行计算(多核、GPU、SIMD)、算法优化 或 编译优化。
举例:对一张超大图像做 FFT 运算,不涉及磁盘和网络,CPU 会跑到 100%,这是 CPU-bound。
3. 对比总结
维度 | I/O-bound | CPU-bound |
---|---|---|
瓶颈 | 外设 I/O(磁盘、网络、设备) | 处理器算力 |
CPU 使用率 | 低,CPU 常常空闲等待 | 高,CPU 长时间接近 100% |
优化手段 | 异步 I/O、多线程、缓存 | 多核并行、GPU 加速、算法优化 |
举例 | 网络请求、数据库查询 | 图像处理、科学计算 |
4. 结合并发/并行
- I/O-bound 任务:更适合 并发(concurrency) → 异步、事件驱动能充分利用等待时间。
- CPU-bound 任务:更适合 并行(parallelism) → 多核分工才能真正加速。
同步/异步 & 阻塞/非阻塞
这个是并发编程里的核心四象限概念,很多人会把它们混在一起。我们拆开来看:
1. 同步 vs 异步(关注 任务完成的通知方式)
同步 (Synchronous)
- 调用者必须等被调用的任务完成,才继续往下执行。
- 强调:谁来负责等结果?是调用者自己。
- 举例:打电话问朋友“今晚吃饭吗”,对方没回答,你就一直等着,不能干别的事。
异步 (Asynchronous)
- 调用者发出请求后,可以继续做别的事,被调用者完成后会主动通知(回调、事件、future/promise)。
- 强调:结果回来时,会有人通知你。
- 举例:发微信问朋友“今晚吃饭吗”,你可以去玩游戏,朋友晚点回你消息。
2. 阻塞 vs 非阻塞(关注 调用时线程的等待状态)
阻塞 (Blocking)
- 调用时,线程/进程会被挂起,直到结果返回。
- 举例:去餐厅点餐后,服务员让你在柜台等,没上菜前你不能离开。
非阻塞 (Non-blocking)
- 调用时,线程不会被挂起,马上返回一个状态值(可能告诉你“没准备好”)。
- 举例:去餐厅点餐,服务员说“稍等一会儿”,你可以回座位刷手机,过一会儿再问。
3. 四象限组合
阻塞 (Blocking) | 非阻塞 (Non-blocking) | |
---|---|---|
同步 (Sync) | 同步阻塞:最常见,比如 read() 默认模式,要等数据读完。 打电话一直等对方说完。 | 同步非阻塞:调用立即返回,但结果没准备好时,需要主动不断重试(轮询)。 你一遍遍打电话过去问“好了吗?” |
异步 (Async) | 异步阻塞:调用后挂起,等回调或事件完成后再恢复。少见。 你微信发消息后强行盯着手机,别的事也不干。 | 异步非阻塞:调用立即返回,结果就绪时通过回调/事件通知你。现代网络编程主流。 你发微信,继续干别的事,朋友回你消息时微信提醒。 |
4. 编程里的例子
同步阻塞
std::string data = socket.read(); // 一直等到有数据
同步非阻塞
if (socket.try_read(data)) { // 没数据就返回 false process(data); }
异步非阻塞
socket.async_read([](std::string data){ process(data); // 数据到的时候回调 });
总结:
- 同步/异步 → 关注 “结果如何交付”(自己等,还是别人通知)。
- 阻塞/非阻塞 → 关注 “调用时线程是否停下来”(停等 vs 马上返回)。