You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:00:49