Python aiofiles 如何与 asyncio 配合实现高并发文件写入

aiofiles.open写文件更慢是因为默认无缓冲,每次write都触发系统调用,且底层依赖线程池而非真正的异步I/O;提升吞吐关键在减少I/O次数、合理限流并发,并根据场景选择同步buffering或asyncio.to_thread。

为什么直接用 aiofiles.open 写文件反而更慢?

因为 aiofiles.open 默认不缓冲,每次 write() 都触发一次系统调用(即使内容很小),在高并发场景下会放大事件循环调度开销。真正提升吞吐的关键不是“用异步函数”,而是减少 I/O 次数 + 合理控制协程并发量。

  • 单个大文件写入:用同步 open() + buffering=8192 通常比 aiofiles 更快
  • 大量小文件并发写入:才适合 aiofiles,但必须配合 asyncio.Semaphore 限流,否则可能耗尽系统文件描述符或触发 OSError: Too many open files
  • aiofiles 底层仍依赖线程池(loop.run_in_executor)模拟异步,不是真正的异步 I/O —— 这点常被忽略

如何安全地并发写入多个文件?

核心是用 asyncio.Semaphore 控制同时打开的文件数,避免资源打满。示例中限制为 10 个并发写操作:

import asyncio
import aiofiles
import os

sem = asyncio.Semaphore(10) # 控制最大并发写入数

async def write_file(filename: str, content: str): async with sem: # 等待许可 try: async with aiofiles.open(filename, "w") as f: await f.write(content) except OSError as e: if e.errno == 24: # Too many open files print(f"警告:{filename} 写入失败 - {e}") raise

启动 100 个写任务

tasks = [writefile(f"out{i}.txt", f"data_{i}") for i in range(100)] await asyncio.gather(*tasks, return_exceptions=True)

  • 不要省略 async with sem,尤其在循环中启动大量任务时
  • return_exceptions=True 防止一个失败导致整个 gather 中断
  • 注意 aiofiles.open(..., "w") 不支持 buffering 参数,无法像同步那样调优缓冲区

写入大字符串时要不要手动分块?

要。如果单次 write() 传入几 MB 的字符串,aiofiles 会把它整个加载进内存再交由线程池处理,容易引发内存峰值。建议按 64KB~1MB 分块写入:

async def write_large_content(filename: str, content: str, chunk_size: int = 1024*64):
    async with aiofiles.open(filename, "w") as f:
        for i in range(0, len(content), chunk_size):
            chunk = content[i:i + chunk_size]
            await f.write(chunk)
  • 分块大小不是越大越好:超过 1MB 可能增加单次线程池阻塞时间
  • 若内容来自生成器(如逐行处理日志),优先用 async for line in ... 流式写入,而非拼成大字符串
  • 对超大文件(>1GB),考虑用同步方式配合 mmapshutil.copyfileobj,异步在此场景无优势

有没有比 aiofiles 更适合高并发写的替代方案?

有,但取决于场景。纯文本批量写入时,asyncio.to_thread + 同步 open 往往更稳、更易调试:

import asyncio

async def sync_write_in_thread(filename: str, content: str): def _write(): with open(filename, "w", buffering=8192) as f: f.write(content) await asyncio.to_thread(_write)

  • to_thread(Python 3.9+)比 aiofiles 更轻量,且能利用 bufferingnewline 等底层优化
  • 如果项目已用 aiofiles,不必强行替换;但新项目遇到写性能瓶颈,先测 to_thread 版本
  • 真正需要异步 I/O 的场景(如网络响应直接落盘、实时日志流),应考虑 uvloop + libuv 绑定方案,而非纯 Python 层的 aiofiles

实际压测中,多数 Web 日志或批处理写入场景,瓶颈不在「是否异步」,而在磁盘随机写延迟和 OS 文件缓存策略。别过早优化 aiofiles 调用本身,先确认是不是真的卡在写文件上。