Python 多进程与多线程混合情况下logging模块死锁问题

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的形式,即从父进程复制相应的资源,创建子进程

最后思考为什么刚才的程序里为啥会死锁:

  1. 线程一直输出日志,logging的处理器一直占用锁,未释放
  2. 创建子进程的时候,子进程复制了父进程的资源,同时也复制了一个锁,注意从源码上来看:这个锁时threading.RLock()函数创建的,也就是必须由本身释放
  3. 此时,子进程的logging使用处理器的时候,首先获取锁,但是这个锁是由父进程创建的,子进程释放不了,所以子进程一直在获取,产生死锁

解决办法:绕过这个问题

了解到产生原因,那么就必然能够解决,当然有很多种解决办法,不过目前我的解决办法是:在子进程中,首先移除处理器,然后重新创建新的处理器。过程如下:

  1. 查看本身的记录器里是否有处理器,如果有,删除处理器
  2. 删除根处理器
  3. 重新创建根处理器
  4. 重新创建本身的处理器

删除根处理器的代码如下所示:

for handler in logging.root.handlers:
    logging.root.removeHandler(handler)

创建处理器的过程,和之前创建的过程一样,注意如果本身没有调用相关的逻辑,那么说明使用的是默认的根处理器,此时删除了,就必须重新创建处理器,使用logging.basicConfig即可

注意

使用字典的形式创建日志可能也会有问题,之前尝试过很多次,不成功,可能是logging.config.dictConfig函数本身会有bug,但是具体原因不清楚。

上一篇:python的日志


下一篇:自动化框架之Python日志模块二次封装技巧分享