直接用 open() 在 async 函数里会阻塞,因其是同步阻塞调用,无法被事件循环调度;正确做法是用 aiofiles.open() 配合 async with 和 await 进行异步文件操作。

为什么直接用 open() 在 async 函数里会阻塞?
因为标准 open() 是同步阻塞调用,哪怕你把它写在 async def 里,事件循环也会被卡住——它不会自动切到其他协程,IO 等待期间整个 asyncio 事件循环就停摆了。这不是语法错误,但完全违背异步初衷。
常见现象:多个文件读取任务并发写成 await asyncio.gather(f1(), f2(), f3()),结果耗时接近串行,CPU 占用低、总耗时高,asyncio 形同虚设。
根本原因:Python 的文件 IO 底层依赖操作系统调用(如 read()、write()),而这些调用默认是阻塞的,无法被 asyncio 直接调度。
如何正确使用 aiofiles 做异步文件读写?
aiofiles 并不是自己实现非阻塞 IO,而是把阻塞调用放到线程池里执行(默认用 loop.run_in_executor()),再用 await 包装成协程接口。所以它“看起来”异步,实际是线程 + 协程的混合方案。
实操要点:
- 必须用
async with aiofiles.open(...),不能用await aiofiles.open(...)—— 后者返回的是个对象,不是协程;前者才触发真正的异步打开 - 读写方法(
.read()、.write())都必须await,它们返回的是协程对象,不是字符串或字节 - 不要混用同步和异步句柄:比如用
aiofiles.open()打开后,再拿它的.file属性去调.read(),会报AttributeError - 二进制模式必须显式指定:
mode="rb"或mode="wb";文本模式下注意编码,默认是utf-8,但不传encoding参数可能在 Windows 上出错
示例:
import aiofiles import asyncio <p>async def read_file(path): async with aiofiles.open(path, mode='r', encoding='utf-8') as f: content = await f.read() # ← 必须 await return content</p><p>async def write_file(path, data): async with aiofiles.open(path, mode='w', encoding='utf-8') as f: await f.write(data) # ← 必须 await </p>
aiofiles 的性能瓶颈在哪?什么场景下反而更慢?
它快的前提是:IO 密集、文件不大、并发数适中(几十以内)。一旦超出这个范围,线程池就成了瓶颈。
容易踩的坑:
- 小文件高频读写(比如每秒上千次 1KB 文件操作):线程创建/切换开销 > IO 节省,比同步还慢
- 大文件单次读取(如读一个 2GB 日志):
await f.read()会一次性加载进内存,可能 OOM;应改用await f.readline()或分块await f.read(8192) - 未限制并发数:
asyncio.gather(*[read_file(p) for p in paths])同时打开几百个文件,线程池打满,系统句柄耗尽,报OSError: [Errno 24] Too many open files - Windows 下默认线程池较小(
asyncio.DefaultEventLoopPolicy在 Windows 上用ProactorEventLoop,对run_in_executor支持略弱),建议显式设置asyncio.get_event_loop().set_default_executor(ThreadPoolExecutor(max_workers=32))
有没有比 aiofiles 更底层/更高效的选择?
纯异步文件 IO 在 Python 标准库中并不存在。Linux 上可通过 os.open() + os.posix_fadvise() + asyncio.loop.create_future() 配合 io_uring(需 kernel 5.1+ 和第三方库如 aiolinux)实现真异步,但生态极不成熟,调试成本高,不推荐日常使用。
目前最务实的路径是:
- 小文件、中等并发 → 用
aiofiles,配好线程池大小 - 大文件流式处理 → 改用同步
open()+yield+asyncio.to_thread()(Python 3.9+),手动控制 chunk 大小和并发粒度 - 日志类追加写 → 用同步
open(..., mode='a')+logging模块的QueueHandler+ 单独线程消费,比纯异步更稳
真正需要警惕的,不是“没用上异步”,而是用错了方式——比如在 Web 请求里为读一个配置文件硬套 aiofiles,反而增加不必要的上下文切换和异常分支。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











