Python多进程多线程场景下logging模块死锁问题排查
从你的描述和gdb调试信息来看,这个死锁问题是多个因素叠加导致的,和logging模块的使用方式、Python 2.7的线程/锁机制以及终端设备特性密切相关,具体分析如下:
核心原因
1. 重复添加Handler引发的多锁竞争死锁
你的get_logger函数存在一个关键问题:每次调用都会给目标logger新增FileHandler和StreamHandler,而非复用已有的Handler。这会导致同一个logger(尤其是默认的root logger)积累大量Handler实例,每个Handler都持有独立的锁。
当多个线程高频调用logging方法时,会遍历logger的所有Handler并依次获取每个Handler的锁。如果两个线程在遍历Handler时,以不同顺序尝试获取锁(比如线程1先拿FileHandler锁再拿StreamHandler锁,线程2先拿StreamHandler锁再拿FileHandler锁),就会触发循环等待死锁——每个线程持有一个锁,同时等待对方持有的另一个锁,最终导致所有线程卡住。
2. StreamHandler的终端写阻塞放大问题
所有进程的StreamHandler都指向同一个终端设备(stderr/stdout),而终端的写操作在输出缓冲区满时会进入阻塞状态。当某个线程持有Handler锁的情况下卡在write系统调用中时,其他线程会因等待该Handler锁而无法继续执行。如果此时还有其他锁被不同线程持有,会进一步加剧死锁的概率。
3. Python 2.7 GIL与锁的交互特性
Python 2.7的GIL在执行阻塞系统调用(比如write)时会释放,让其他线程有机会获取GIL。但如果线程在持有Handler锁的情况下进入阻塞的系统调用,其他线程虽然能拿到GIL,但会卡在获取Handler锁的步骤上。当多个线程陷入“拿GIL→等Handler锁→释放GIL→再拿GIL”的循环,再加上锁顺序不一致的情况,最终会导致整个进程的线程全部停摆。
解决方案
1. 修复Logger初始化逻辑,避免重复添加Handler
修改get_logger函数,先检查logger是否已有Handler,仅在无Handler时才添加,避免重复创建:
def get_logger(log_level, file_name='', log_name=''): if len(log_name) != 0: logger = logging.getLogger(log_name) else: logger = logging.getLogger() # 关键:避免重复添加Handler if logger.handlers: logger.setLevel(logger_state[log_level]) return logger logger.setLevel(logger_state[log_level]) formatter = logging.Formatter('%(asctime)s [%(levelname)s][%(name)s:%(funcName)s():%(lineno)s] - %(message)s') # file handler if len(file_name) != 0: fh = logging.FileHandler(file_name) fh.setLevel(logging.DEBUG) fh.setFormatter(formatter) logger.addHandler(fh) # console handler(不需要可移除) console_out = logging.StreamHandler() console_out.setLevel(logging.DEBUG) console_out.setFormatter(formatter) logger.addHandler(console_out) return logger
2. 给每个进程使用独立的Logger实例
不要使用默认的root logger,给每个进程指定唯一的log_name(比如包含进程ID),确保不同进程的Logger完全隔离,避免跨进程的Handler干扰:
import os logger = get_logger(log_level, file_name=f"process_{os.getpid()}.log", log_name=f"process_{os.getpid()}")
3. 移除不必要的StreamHandler
如果你的场景不需要控制台输出,直接删掉StreamHandler的代码,减少锁竞争和终端写阻塞的概率。如果必须保留控制台输出,可以让每个进程的StreamHandler输出到不同的终端或文件,避免共享设备的阻塞问题。
4. 考虑使用进程安全的日志方案
Python标准库的logging模块本身不是进程安全的(进程间不共享锁),如果多进程场景下日志写入冲突严重,可以使用第三方库如concurrent-log-handler,它实现了进程安全的文件日志处理;或者使用logging.handlers.QueueHandler+QueueListener的组合,把日志事件放到队列中,由单个线程统一写入,减少锁竞争。
5. 升级到Python 3(长期解决方案)
Python 3对GIL机制做了大幅优化(尤其是Python 3.2+的GIL调度),同时logging模块也修复了一些线程安全相关的问题,能从根本上减少这类死锁的发生概率。
内容的提问来源于stack exchange,提问作者Arkady

