前言

在深入理解协程(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-boundCPU-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 马上返回)。