日志记录适用于多处理,但不适用于loky

时间:2019-11-13 12:24:08

标签: python logging multiprocessing

我正试图从传递给多处理/ loky的函数中向stdout生成日志输出。日志记录仅适用于多处理,不适用于loky。相反,打印对两者都有效。我想了解两个框架之间的主要区别是什么,这是造成此问题的原因。另外,我该如何解决?

示例:

import multiprocessing
from loky import get_reusable_executor
import logging

logger = logging.getLogger('')
f_handler = logging.StreamHandler()
f_format = logging.Formatter('> %(name)s - %(message)s')
f_handler.setFormatter(f_format)
logger.addHandler(f_handler)

logger.setLevel('INFO')

def func(*args):
    print('Print in func.', flush=True)
    logger.info('Logger in func.')

multiprocessing

logger.name = 'multiprocessing'
pool = multiprocessing.Pool(2)
pool.map(func, range(2))
pool.close()
pool.join()

打印:

Print in func.
Print in func.
> multiprocessing - Logger in func.
> multiprocessing - Logger in func.

loky

logger.name = 'loky'
executor = get_reusable_executor(max_workers=2, timeout=2)
list(executor.map(func, range(2)))

打印:

Print in func.
Print in func.

1 个答案:

答案 0 :(得分:2)

Fork vs Fork + Exec

multiprocessingloky示例的行为有所不同,因为它们使用两种不同的方法启动新流程。

根据loky README

所有进程都在POSIX系统上使用fork + exec启动。这样可以确保与第三方库的交互更加安全。相反,multiprocessing.Pool默认情况下使用不带exec的fork,从而导致第三方运行时崩溃(例如OpenMP,macOS Accelerate ...)。

使用fork和fork + exec的区别在于,当仅使用fork时,将复制现有进程,并且新创建的(子)进程将与原始进程一起继续运行原始程序的副本。这就是您的multiprocessing示例可以访问最初配置的处理程序的原因。

loky使用fork + exec(但是在Windows上不是 )时,分叉的过程实际上会启动一个新的独特的Python解释器,从而失去对许多现有解释器的访问权限logger之类的对象。具体来说,当在fork_exec中调用popen_loky_posix.py时,发生了对os.execve的调用。

示例解决方案

我正在使用以下命令(在5.3.0-40通用#32-Ubuntu SMP x86_64 GNU / Linux上):

Python version: 3.7.3 | packaged by conda-forge | (default, Jul  1 2019, 21:52:21) 
[GCC 7.3.0]
loky version: 2.6.0
import logging
import multiprocessing
import os
from time import sleep

import loky
from loky import get_reusable_executor


def get_logger():
    print("Getting a new logger.")
    logger = logging.getLogger("")
    print("PID:", os.getpid())
    print("logger id:", id(logger))
    print("Pre-existing handlers:", logger.handlers)
    f_handler = logging.StreamHandler()
    f_format = logging.Formatter("> %(name)s - %(message)s")
    f_handler.setFormatter(f_format)
    logger.addHandler(f_handler)

    logger.setLevel("INFO")

    return logger


logger = get_logger()


def func(*args):
    print("PID:", os.getpid())
    print("Print in func.", flush=True)
    logger.info("Logger in func.")


def loky_func(*args):
    # sleep(0.1)
    logger = get_logger()
    logger.name = "loky"
    print("Print in func.", flush=True)
    logger.info("Logger in func.")


def multiprocessing_logging():
    logger.name = "multiprocessing"
    logger.info("Using multiprocessing")
    pool = multiprocessing.Pool(2)
    pool.map(func, range(2))
    pool.close()
    pool.join()


def loky_logging():
    logger.name = "loky"
    logger.info("Using loky")
    executor = get_reusable_executor(max_workers=2, timeout=2)
    list(executor.map(loky_func, range(2)))


if __name__ == "__main__":
    print("-" * 82)
    multiprocessing_logging()
    print("-" * 82)
    loky_logging()

运行以上结果:

Getting a new logger.
PID: 31034
logger id: 140272374338728
Pre-existing handlers: []
----------------------------------------------------------------------------------
> multiprocessing - Using multiprocessing
PID: 31070
Print in func.
PID: 31071
Print in func.
> multiprocessing - Logger in func.
> multiprocessing - Logger in func.
----------------------------------------------------------------------------------
> loky - Using loky
Getting a new logger.
PID: 31076
logger id: 139836973549720
Pre-existing handlers: []
Print in func.
> loky - Logger in func.
Getting a new logger.
PID: 31076
logger id: 139836973549720
Pre-existing handlers: [<StreamHandler <stderr> (NOTSET)>]
Print in func.
> loky - Logger in func.
> loky - Logger in func.

因此,可以在loky辅助函数loky_func中获取并配置新的记录器是一种可能的解决方法。

有趣的是,loky选择根本不产生2个进程(与multiprocessing相对),从{{1}中相同的PID和记录器ID编号可以看出}部分输出。其结果是,先前的记录器被重用,导致第二次运行loky时出现2个StreamHandler处理程序(因此输出重复)。

如果我们像这样修改loky_func

loky_func

我们发现现在对应的输出是

> loky - Using loky
Getting a new logger.
PID: 31390
logger id: 140144439347296
Pre-existing handlers: []
Print in func.
> loky - Logger in func.
Getting a new logger.
PID: 31391
logger id: 140330455283864
Pre-existing handlers: []
Print in func.
> loky - Logger in func.

表示现在产生了2个进程。每个人都检索自己的记录器,并向其中添加了正确的处理程序,从而产生了预期的行为。

要使此方法更可靠,您必须检查并修改处理程序列表def loky_func(*args): sleep(0.1) # 'Long' computation. logger = get_logger() logger.name = "loky" print("Print in func.", flush=True) logger.info("Logger in func.") ,而不是简单地添加处理程序。使这样的方法线程和过程安全将需要使用更多工具,例如使用logger.handlers时使用multiprocessing.Queue或使用multiprocessing时使用multiprocessing.Manager的共享队列(但可能还有更多工具)有效的方法)。

日志记录软件包loguru实现了用于记录文件的前一种方法(在loky中使用enqueue=True参数时使用队列),从而在使用{时允许线程和进程进行安全记录{1}}。但是,由于它不使用多处理管理器,因此在使用logger.add时这种方法会中断。可以通过手动提供一个共享的Manager Queue作为multiprocessing辅助函数的参数,然后使用它在其中设置处理程序来解决此问题。

相关问题