发布于2026-07-19 阅读(0)
扫一扫,手机访问
多线程同时写入同一个文件,这事儿看着简单,实则是个坑。不少开发者一上来就写个循环,开几个线程,自信满满地往一个文件里“咣咣咣”写东西,结果日志一打开,错乱、覆盖、乱码,什么妖魔鬼怪都来了。问题出在哪?不是Python的锅,是POSIX文件I/O模型本身就没打算让你这么干。

多个线程调用 open(..., 'a') 或 f.write() 时,操作系统层面的文件偏移量(file offset)和缓冲区状态不一致,导致内容错乱、覆盖或丢失。这不是 Python 的 bug,而是 POSIX 文件 I/O 模型决定的:即使追加模式('a'),write() 系统调用本身不是原子操作,尤其在高并发小写入场景下极易复现。
常见错误现象包括:
"user_id=123, status=ok" 写成 "user_id=123, stat" 和 "us=ok")关键点:锁对象必须是所有线程共享的同一个实例,且要包裹整个写入动作(打开 + 写 + 关闭),不能只锁 f.write()。否则仍可能多个线程同时执行 open(),拿到各自独立的文件句柄,锁就失效了。
正确做法:
threading.Lock() 对象,不要在每个线程里新建with lock: 包裹从 open() 到 f.close() 的全过程print(..., file=f) 这类隐式写法,统一用 f.write() + 手动换行示例:
import threading
log_lock = threading.Lock() # 共享锁
def write_log(message):
with log_lock: # 必须在这里进入临界区
with open("app.log", "a") as f:
f.write(f"[{threading.current_thread().name}] {message}\n")
加锁能保数据不乱,但会显著降低吞吐量——所有写请求串行化。如果每秒写几百次,瓶颈立刻出现在锁竞争上。
更严重的是:如果某个线程在写文件时抛异常(比如磁盘满、权限被撤),而你没在 with 块里处理,锁可能被永久持有(虽然 Python 的 with 在绝大多数情况下能保证释放,但极端异常如 os._exit() 或 SIGKILL 仍可能绕过)。所以:
with open() 而非手动 f = open(); ...; f.close()os.access(path, os.W_OK))如果你只是记日志,别自己造轮子。Python 标准库的 logging 模块默认就是线程安全的,底层已用锁保护 handler 的 emit() 方法。
直接用它更可靠:
import logging
logging.basicConfig(
filename="app.log",
level=logging.INFO,
format="%(asctime)s [%(threadName)s] %(message)s"
)
logging.info("User login succeeded") # 自动线程安全
自建 threading.Lock 只应在以下情况考虑:需要精确控制写入格式(比如二进制协议包)、必须绕过 logging 的格式化开销、或写入非文本文件(如 SQLite 的 WAL 模式除外,那该用数据库锁)。
真要自己锁文件,记住:锁的是「写行为」,不是「文件路径」;锁粒度越粗越安全,也越慢;而 logging 已经帮你平衡好了这个权衡。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8