文章
我们在说同步/异步时到底是在说什么?
同步/异步 说的是「调用后是否等待完成」
目录
当我们讨论「同步」与「异步」时,核心是在说调用方发起一项操作后,是否要等这项操作完成才能继续执行下一步。下面分几个维度来说明它们的含义和典型使用场景。#
一、概念区分#
| 方面 | 同步(Synchronous) | 异步(Asynchronous) |
| 调用语义 | 发起调用后,会**阻塞**调用者,直到操作完成并返回结果 | 发起调用后**不阻塞**调用者,立即返回,操作在后台进行 |
| 线程行为 | 同一线程负责整个操作,从开始到结束都在该线程上执行 | 发起线程发出请求后可去做其他事,真正执行操作的可能是后台线程或事件回调 |
| 编程模型 | 直接写:`let r = f(); // f 完成后才有结果` | 通过回调、Future/Promise、事件通知等拿到结果 |
| 资源占用 | 阻塞期间可能浪费线程/CPU | 等待期间线程可复用、CPU 可执行别的任务 |
二、同步 vs 异步 的使用场景#
1. 同步场景#
- CPU 密集型计算
- 例如矩阵乘法、图像渲染等,执行时间较长,但基本不会做 I/O。
- 用同步更简单,数据在本地,阻塞线程做计算,直观且易于调试。
- 脚本型、一次性批处理
- 如脚本按顺序执行任务,不需要并发或并行,写同步调用更直观。
- 事务性或串行依赖
- 多步操作必须前一步完成才能进行下一步(如数据库事务中的多条 SQL 顺序执行)。
2. 异步场景#
- 高并发 I/O 密集型服务
- 网络服务器(HTTP、WebSocket、Redis)、代理、爬虫等,大量连接/请求轮流等待网络数据。
- 异步让单线程或少量线程能同时管理成千上万连接,节省上下文切换和线程资源。
- GUI 与事件驱动
- 桌面/移动应用的界面线程需要保持流畅响应,任何耗时操作都应异步(后台线程或异步 API),否则界面卡死。
- 后台定时/并行任务
- 比如同时向多个外部 API 发请求,用
Promise.all或tokio::join!并发调用,最后汇总结果。
- 比如同时向多个外部 API 发请求,用
三、同步/异步 指的到底是什么?#
- 调用者视角:
- 同步:调用
foo(),会阻塞当前执行流,直到foo完成并返回。 - 异步:调用
foo_async(),立即拿到一个“将来才会有结果”的句柄(Future/Promise/Callback),调用者可以继续下一步,稍后通过.await、回调或事件处理拿到结果。
- 同步:调用
- 执行机制:
- 同步 完全在线程栈上执行;
- 异步 将操作注册到某种调度器(事件循环、线程池),由它在后台下发和唤醒。
- 阻塞 vs 挂起:
- 阻塞(block)会让线程进入内核等待,无法进行任何其他工作;
- 挂起(suspend)只是把当前任务挂起到调度队列,线程可以去执行别的任务。
四、如何选?#
| 考量维度 | 选同步 | 选异步 |
| 调用顺序必须严格 | ✔ | × |
| 逻辑非常简单 | ✔(少量 I/O) | — |
| I/O 操作频繁 | ×(线程多、切换多,资源浪费) | ✔(节省线程、响应高并发) |
| UI 或需保持响应 | ×(易卡死) | ✔(后台执行,不影响主线程) |
| 平台/语言支持 | 通用 | 需语言或框架支持异步模型(如 Tokio、Node.js、.NET async) |
举例#
- 同步(Python、Rust、Java 都可):
// 顺序读两个文件 let s1 = std::fs::read_to_string("a.txt")?; let s2 = std::fs::read_to_string("b.txt")?; ```
- 异步(Rust Tokio 示例):
let (s1, s2) = tokio::join!( tokio::fs::read_to_string("a.txt"), tokio::fs::read_to_string("b.txt") ); ```#
五、总结#
- 同步/异步 说的是「调用后是否等待完成」。
- 同步 编程简单直观,但在 I/O 密集场景会浪费线程资源。
- 异步 能高效利用线程、处理高并发 I/O,但对编程模型有一定复杂度。
- 选择时应根据 业务特性(CPU-bound vs I/O-bound)、并发需求、平台支持 来决定。