同步写入的局限:阻塞调用、资源浪费、性能差

说到同步写入,可能很多新手觉得它很直观:写个文件嘛,调个 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 只有几千,线程再多也白搭。

来,看个直观的示意图:

同步写入的瓶颈流程 应用线程 发出 write() 调用 内核缓冲区 数据拷贝 + 等待刷盘 磁盘 / 存储设备 机械寻道 / SSD 写入 刷盘操作 线程阻塞 等待 IO 完成 唤醒 核心问题 线程在 write() 处阻塞 → CPU 利用率低 → 内存的拷贝和等待浪费 → 吞吐受限

一句话总结:同步写入让线程彻底“静止”,不能做其他任何有用的工作。这在用户交互场景下会直接卡界面,在后台服务场景下会让吞吐上不去。

避坑指南:我曾经踩过的坑

  • 不要盲目用 O_SYNC 或 fsync:每次写入都刷盘,性能直接崩。我见过一个数据库 WAL 日志,开了 O_SYNC 写一行事务日志延迟飙升到 20ms,后来改成异步 buffer 定期 fsync,延迟降到 1ms 以下。
  • 小心“写放大”:小数据块同步写入,每次都要做磁盘元数据更新和日志写入。我曾经测过 4KB 写 vs 64KB 写,前者 IOPS 只有后者的 1/8。
  • 记住这个原则:如果一次写入操作之后不需要立刻读回数据,就不要用同步写。交给异步 buffer 排队处理,性能至少提升一个数量级。

为什么我们不用同步写了?

说白了,现代 IO 硬件(NVMe SSD、高带宽网络)的延迟已经很低,但软件层面的同步模型仍然按 1990 年代磁盘时代的路子走——每次操作都要等待硬件完成。这中间的差距,就是我们需要引入异步缓冲区来填补的地方。

注意:并不是说同步写入一无是处。在极低并发、对数据持久性要求极高的场景(比如金融交易日志),同步写入并配合 fsync 反而是必要的。但大部分业务场景,尤其是 Web 服务和数据处理管道,用同步写就是自讨苦吃。

下一部分咱们会具体讲如何用异步缓冲区来突破这个瓶颈。但首先得先理解你今天看到的这三个局限:阻塞、浪费、低效。理解了它们,你才能在实际项目中做好取舍。