Python 多进程与多线程混合情况下logging模块死锁问题
死锁问题复现
在使用多线程与多进程混杂使用的情况下,有时启动多进程不成功,如下所示:
from multiprocessing import Pool
import threading
import logging
import os
logging.basicConfig(filename='./info.log', level=logging.INFO)
_logger = logging.getLogger(__name__)
def test_log():
while 1:
_logger.info('hello world')
def test_subprocess():
_logger.info(f"{os.getpid()} start successful")
if __name__ == "__main__":
threading.Thread(target=test_log, daemon=True).start()
pool = Pool(3)
for i in range(3):
pool.apply_async(func=test_subprocess)
while 1:
pass
预期结果为:启动多个进程,然后控制台输出 XXX start successful
实际结果:没有出现相应的结果,原因为logging并非进程安全的,所以导致死锁
原因详解
首先要了解logging是线程安全的,这是因为它自己本身有线程锁,每次在调用处理器的时候,首先获取锁,然后记录到日志里,最后解锁。
其次,需要了解multiprocess创建进程的原理是什么,具体的我也不是很清楚,但是从官方文档来看,multiprocess创建子进程是通过fork的形式,即从父进程复制相应的资源,创建子进程
最后思考为什么刚才的程序里为啥会死锁:
- 线程一直输出日志,logging的处理器一直占用锁,未释放
- 创建子进程的时候,子进程复制了父进程的资源,同时也复制了一个锁,注意从源码上来看:这个锁时threading.RLock()函数创建的,也就是必须由本身释放
- 此时,子进程的logging使用处理器的时候,首先获取锁,但是这个锁是由父进程创建的,子进程释放不了,所以子进程一直在获取,产生死锁
解决办法:绕过这个问题
了解到产生原因,那么就必然能够解决,当然有很多种解决办法,不过目前我的解决办法是:在子进程中,首先移除处理器,然后重新创建新的处理器。过程如下:
- 查看本身的记录器里是否有处理器,如果有,删除处理器
- 删除根处理器
- 重新创建根处理器
- 重新创建本身的处理器
删除根处理器的代码如下所示:
for handler in logging.root.handlers:
logging.root.removeHandler(handler)
创建处理器的过程,和之前创建的过程一样,注意如果本身没有调用相关的逻辑,那么说明使用的是默认的根处理器,此时删除了,就必须重新创建处理器,使用logging.basicConfig即可
注意
使用字典的形式创建日志可能也会有问题,之前尝试过很多次,不成功,可能是logging.config.dictConfig函数本身会有bug,但是具体原因不清楚。