我正试图从传递给多处理/ 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.
答案 0 :(得分:2)
multiprocessing
和loky
示例的行为有所不同,因为它们使用两种不同的方法启动新流程。
根据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
辅助函数的参数,然后使用它在其中设置处理程序来解决此问题。