同步写入的局限:阻塞调用、资源浪费、性能差
说到同步写入,可能很多新手觉得它很直观:写个文件嘛,调个 write() 就完了。嗯,我以前也是这么想的。直到我第一次在线上系统里遇到高并发写入导致的卡顿,才意识到这个“直觉”有多坑。
同步写入,说白了就是你的线程在调用写入接口后,必须死等操作系统把数据从用户态拷贝到内核态、再真正写到磁盘(或者网络缓冲区),然后才返回。这期间你的线程啥也不能干——就挂在那。
它会带来三个核心问题。咱们一个一个说。
阻塞调用——线程被“卡死”
先看个典型的同步写入代码:
void write_data(int fd, const char* data, size_t len) {
ssize_t written = write(fd, data, len);
if (written < 0) {
perror("write failed");
exit(1);
}
// 只有这里返回,才能继续做其他事
printf("write finish\n");
}
调用 write() 之后,线程就进入阻塞状态。操作系统会把该线程移出运行队列,直到写入完成才会唤醒。在 IO 密集的应用里,这会引发严重的上下文切换开销。
我在项目中遇到过一个日志系统,写一行日志就要等一次磁盘同步。QPS 一高,线程全部堆在 write 上,CPU 忙得全是切换,实际写数据的比例不到 10%。后来换成异步 buffer,吞吐直接翻了三倍。
阻塞调用本质上浪费了线程执行的“时间片”。你想想看,一个 4 核机器开 8 个线程,如果每个线程都在执行阻塞写操作,那它可能连一个核都占不满——但线程切换的开销却一点不少。
资源浪费——内存、文件描述符、CPU 都在空转
同步写入的第二个局限,是资源无法充分利用。我们来列一张表对比一下同步和异步的场景:
| 维度 | 同步写入 | 异步写入(buffer + 非阻塞/io_uring) |
|---|---|---|
| 线程占用 | 每个请求独占一个线程,直到 IO 完成 | 请求提交后线程立刻返回,可处理其他任务 |
| 内存拷贝次数 | 至少 2 次(用户态→内核态→磁盘) | 可合并多次小写为一次大数据块写入,减少拷贝 |
| CPU 利用率 | 大量时间花在上下文切换和等待通知上 | 充分利用 CPU 做计算或处理新请求 |
| 文件描述符管理 | 需要为每个连接保持一个阻塞 fd | 可使用多路复用或共享 fd |
我曾经接手过一个老项目,为了“简单”,每个请求用 fork() 创建一个子进程去写文件。结果同时几千个连接时,系统直接 OOM 了——每个子进程都要占几 MB 的栈空间。这其实就是同步模型的资源浪费在极端情况下的表现。
性能差——延迟和吞吐的双重损失
性能差体现在两个指标上:延迟抖动和吞吐上限。
- 延迟抖动:磁盘 IO 本身有毫秒级延迟,如果同步写入时恰好遇到磁盘繁忙(比如正在刷脏页),一次 write 可能卡住几十毫秒甚至几百毫秒。你的客户端所有操作都得排队等他。
- 吞吐上限:由于每个请求都要等待 IO 完成,系统的吞吐量被
1 / 单次写入延迟限制。就算你有 64 个线程,物理磁盘的 IOPS 只有几千,线程再多也白搭。
来,看个直观的示意图:
一句话总结:同步写入让线程彻底“静止”,不能做其他任何有用的工作。这在用户交互场景下会直接卡界面,在后台服务场景下会让吞吐上不去。
避坑指南:我曾经踩过的坑
- 不要盲目用
O_SYNC或fsync:每次写入都刷盘,性能直接崩。我见过一个数据库 WAL 日志,开了O_SYNC写一行事务日志延迟飙升到 20ms,后来改成异步 buffer 定期 fsync,延迟降到 1ms 以下。 - 小心“写放大”:小数据块同步写入,每次都要做磁盘元数据更新和日志写入。我曾经测过 4KB 写 vs 64KB 写,前者 IOPS 只有后者的 1/8。
- 记住这个原则:如果一次写入操作之后不需要立刻读回数据,就不要用同步写。交给异步 buffer 排队处理,性能至少提升一个数量级。
为什么我们不用同步写了?
说白了,现代 IO 硬件(NVMe SSD、高带宽网络)的延迟已经很低,但软件层面的同步模型仍然按 1990 年代磁盘时代的路子走——每次操作都要等待硬件完成。这中间的差距,就是我们需要引入异步缓冲区来填补的地方。
注意:并不是说同步写入一无是处。在极低并发、对数据持久性要求极高的场景(比如金融交易日志),同步写入并配合 fsync 反而是必要的。但大部分业务场景,尤其是 Web 服务和数据处理管道,用同步写就是自讨苦吃。
下一部分咱们会具体讲如何用异步缓冲区来突破这个瓶颈。但首先得先理解你今天看到的这三个局限:阻塞、浪费、低效。理解了它们,你才能在实际项目中做好取舍。