首选方案是multiprocessing.Queue + QueueListener,它将文件写入收归单一线程,子进程仅发消息,避免竞争;需主进程创建队列并启动监听,子进程配置QueueHandler,退出前调用stop()。

不能直接共享同一个 FileHandler 实例写入同一文件——这是根本错误起点。多进程并发写同一文件句柄,必然导致日志错乱、截断、空行甚至 OSError: [Errno 9] Bad file descriptor,不是配置问题,是机制冲突。
为什么 multiprocessing.Queue + QueueListener 是首选方案
它把“写文件”这个危险操作收归到单一线程(或单个进程),子进程只负责发消息,完全规避文件竞争。官方 logging 模块原生支持,且能无缝对接 RotatingFileHandler、自定义格式、编码等所有功能。
- 主进程中创建
multiprocessing.Queue(),再用它初始化QueueListener,绑定一个FileHandler或RotatingFileHandler - 调用
queue_listener.start()启动监听(注意:这是线程,不是进程;若需进程模型,得额外包装) - 每个子进程里,先清空 root logger 的 handler(
logger.handlers.clear()),再添加QueueHandler(queue) - 子进程调用
logging.info()时,LogRecord自动序列化进队列,由监听端反序列化后统一落盘 - 务必在主进程退出前调用
queue_listener.stop(),否则子进程可能卡在queue.put()上
用 multiprocessing.Lock 做文件级加锁的适用边界
仅适合低频、短时、进程数 ≤ 3 的调试场景。一旦日志量上升或进程变多,性能会断崖式下跌,且存在死锁风险——比如子进程崩溃未释放锁,后续进程永久阻塞。
- 必须用
multiprocessing.Lock(),threading.Lock()在多进程下完全无效 - 锁对象需通过
Process构造函数或initializer传入子进程,不能在子进程中新建(否则各锁互不感知) - 每次写日志前必须
with lock:,而不是手动acquire()/release(),避免异常导致锁残留 -
RotatingFileHandler的轮转逻辑本身不安全,即使加锁也无法保证 baseFilename 切换原子性,所以只能配FileHandler
按进程 ID 分离日志文件的实际代价
看似简单,但“分离”不等于“解决”。每个子进程写 f"worker_{os.getpid()}.log" 确实零冲突,可一旦需要查问题,就得手动 grep -r "error" *.log,实时追踪几乎不可能。
立即学习“Python免费学习笔记(深入)”;
- 子进程中必须重新调用
logging.basicConfig()或显式addHandler(),不能复用父进程 logger - 若用
concurrent.futures.ProcessPoolExecutor,得在initializer里完成 logger 配置,否则子进程会 fallback 到默认StreamHandler - 日志路径需确保目录存在,否则
FileHandler初始化失败会静默吞掉异常,导致你以为有日志实际没输出 - 别指望靠命名规则自动合并——不同进程的
asctime时间戳可能因系统调度偏差几十毫秒,排序不准
真正难的不是选哪种方案,而是意识到:日志顺序性 ≠ 时间戳精确对齐,而是“入队顺序可重现”。QueueHandler 天然保序,而文件锁或分文件都做不到这点。只要没在主进程里漏掉 queue_listener.stop() 或子进程里忘了清理 handler,这套组合就是目前最稳的落地解法。


















