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

Python多进程使用Logging官方cookbook示例触发死锁问题

Python多进程QueueHandler日志死锁问题

问题现象

多进程场景下按照官方推荐方案使用QueueHandler+QueueListener实现日志时,若日志监听线程先于子进程启动,会触发死锁,和“队列消费者应先于生产者启动”的常规认知矛盾。

复现代码

以下为调整启动顺序的官方Logging Cookbook示例,仅将日志监听线程的启动位置从子进程启动后移至子进程启动前:

import logging
import logging.config
import logging.handlers
from multiprocessing import Process, Queue
import random
import threading
import time

def logger_thread(q):
    while True:
        record = q.get()
        if record is None:
            break
        logger = logging.getLogger(record.name)
        logger.handle(record)


def worker_process(q):
    qh = logging.handlers.QueueHandler(q)
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    root.addHandler(qh)
    levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
              logging.CRITICAL]
    loggers = ['foo', 'foo.bar', 'foo.bar.baz',
               'spam', 'spam.ham', 'spam.ham.eggs']
    for i in range(100):
        lvl = random.choice(levels)
        logger = logging.getLogger(random.choice(loggers))
        logger.log(lvl, 'Message no. %d', i)

if __name__ == '__main__':
    q = Queue()
    d = {
        'version': 1,
        'formatters': {
            'detailed': {
                'class': 'logging.Formatter',
                'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
            }
        },
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
                'level': 'DEBUG',
            },
            'file': {
                'class': 'logging.FileHandler',
                'filename': 'mplog.log',
                'mode': 'w',
                'formatter': 'detailed',
            },
            'foofile': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-foo.log',
                'mode': 'w',
                'formatter': 'detailed',
                'level': 'DEBUG'
            },
            'errors': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-errors.log',
                'mode': 'w',
                'level': 'DEBUG',
                'formatter': 'detailed',
            },
        },
        'loggers': {
            'foo': {
                'handlers': ['foofile']
            }
        },
        'root': {
            'level': 'DEBUG',
            'handlers': ['console', 'file', 'errors']
        },
    }
    logging.config.dictConfig(d)
    lp = threading.Thread(target=logger_thread, args=(q,))

    lp.start() # 日志线程在子进程启动前启动
    
    workers = []
    for i in range(5):
        wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
        workers.append(wp)
        wp.start()
    
    # 官方示例中日志线程在此处启动
    #lp.start()

    for wp in workers:
        wp.join()
    q.put(None)
    lp.join()

初步排查

移除Logging相关逻辑,简化为「独立线程从队列读取数据写入文件、子进程作为生产者向队列投递数据」的场景后,死锁完全消失,初步判断问题不在多进程Queue本身,而在Logging库实现。简化版无死锁代码如下:

from multiprocessing import Process, Queue
import threading
import time

def queue_listener_thread(q):
    with open('test.log', 'w') as f:
        while True:
            record = q.get()
            if record is None:
                break
            time.sleep(0.001)
            f.write(f'{str(record)}\n')


def worker_process(q):
    time.sleep(0.1)
    for i in range(100):
        time.sleep(0.001)
        q.put_nowait(i)


if __name__ == '__main__':
    q = Queue()
    lp = threading.Thread(target=queue_listener_thread, args=(q,))
    lp.start()
    workers = []
    for i in range(5):
        wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
        workers.append(wp)
        wp.start()

    for wp in workers:
        wp.join()
    q.put(None)
    lp.join()

运行环境

  • 业务场景:日志模块在应用启动时完成初始化,后续按需启动、销毁子进程,为生产环境普遍使用模式
  • 系统环境:Fedora Linux 35
  • Python版本:3.10.5

根因定位过程

通过gdb抓取死锁时的Python调用栈如下:

(gdb) py-bt
Traceback (most recent call first):
  File "/usr/lib64/python3.10/logging/__init__.py", line 1084, in flush
    self.stream.flush()
  File "/usr/lib64/python3.10/logging/__init__.py", line 1104, in emit
    self.flush()
  File "/usr/lib64/python3.10/logging/__init__.py", line 1218, in emit
    StreamHandler.emit(self, record)

最初误判问题由文件IO触发,实际阻塞点为StreamHandler对应的标准输出打印逻辑。将简化版代码中的写文件逻辑替换为直接print输出到屏幕后,死锁可稳定复现,复现代码如下:

from multiprocessing import Process, Queue
import threading

def queue_listener_thread(q):
    while True:
        record = q.get()
        if record is None:
            break
        print(f'{str(record)}')


def worker_process(q):
    for i in range(100):
        q.put_nowait(i)


if __name__ == '__main__':
    q = Queue()
    lp = threading.Thread(target=queue_listener_thread, args=(q,))
    lp.start()
    workers = []
    for i in range(5):
        wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
        workers.append(wp)
        wp.start()

    for wp in workers:
        wp.join()
    q.put(None)
    lp.join()

内容的提问来源于stack exchange,提问作者MegaMax

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:06:04