我正在尝试构建一个带有记录器的小型 GUI 应用程序,该应用程序在某些时候会对多组数据(最多数百组)进行一些耗时的操作。当然,我想使用多处理来帮助加快速度。我正在遵循 Python 文档中的Logging Cookbook(第二个示例)中给出的示例,并尝试找出如何将其应用到我的代码中。在这个精简的最小示例中,单击构建按钮应该只记录一些消息。对于对当前主题有更多了解的人来说,问题是显而易见的,那就是它没有按预期工作。应用程序仅将 5 条消息中的 3 条打印到控制台,并且恰好零被添加到日志文件中。
显然,我所期望的是所有 5 条消息都将使用在
logger
中创建的 gui.py
实例进行记录。
我尝试过合并方法,将方法从类中移出到模块级函数中,在不同的位置创建
Queue
/记录器,并将第一个记录器实例作为参数传递。到目前为止,我尝试过的所有操作要么导致相同的结果,要么抛出 pickling
错误并最终以 EOFError
结束。给出的代码只是最新的修订版本,不会引发异常。
我只是想找到一些关于我“哪里”搞砸了的方向。如果重要的话,这是在 Windows 10 上使用 Python 3.12。
# gui.py
from multiprocessing import Queue
import tkinter as tk
from tkinter import ttk
import builder
import logger
class Gui(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
btn = ttk.Button(self, text='Build', command=lambda: builder.Build(q))
btn.pack()
log.configure(q)
self.mainloop()
if __name__ == '__main__':
q = Queue()
log = logger.Logger()
Gui()
# logger.py
import logging
import logging.handlers
class Logger:
def __init__(self):
self.logger = logging.getLogger('dcre')
self.logger.setLevel('DEBUG')
self.log = self.logger.log
def configure(self, q):
self.q = q
self.qh = logging.handlers.QueueHandler(self.q)
self.file = logging.FileHandler('log.log')
self.file.setLevel('DEBUG')
self.logger.addHandler(self.qh)
self.logger.addHandler(self.file)
logging.basicConfig(level='INFO')
# builder.py
import logging
from multiprocessing import Process
import threading
class Build:
def __init__(self, q):
self.queue = q
self.logger = logging.getLogger('dcre')
workers = []
for i in range(5):
wp = Process(target=self.foo, args=(i,))
workers.append(wp)
wp.start()
lp = threading.Thread(target=self.logger_thread)
lp.start()
for wp in workers:
wp.join()
self.queue.put(None)
lp.join()
def logger_thread(self):
while True:
record = self.queue.get()
if record is None:
break
self.logger.handle(record)
def foo(self, i):
msgs = (
(10, "This is a DEBUG message. You shouldn't see this."),
(20, 'This is an INFO message. Just so you know.'),
(30, 'This is a WARNING message. Be careful, yo.'),
(40, 'This is an ERROR message. Man, you done messed up.'),
(50, 'This is a CRITICAL message. Game over!')
)
self.logger.log(*msgs[i])
注意:记录器的
configure
方法的存在只是为了延迟配置,直到创建GUI之后,以便它可以访问文本小部件以供自定义处理程序写入。
在进程之间共享记录器对象可能会变得很奇怪。
我建议使用 QueueHandler 类,因为您已经开始这样做,然后使用顶级日志记录。然后在主队列记录器上设置所有配置设置。这适用于 python 3.11.9
例如
import multiprocessing
import logging
import logging.config
from logging.handlers import QueueListener
from logging import StreamHandler
import atexit
STD_FORMAT_STR = r"[%(levelname)s @ %(name)s|%(filename)s|L%(lineno)d|%(asctime)s|%(process)d]: %(message)s"
STD_DATE_FORMAT_STR = "%Y-%m-%dT%H:%M:%S%z"
def create_share_logging_queue():
### CREATE THE SHARED LOGGING QUEUE
loggingQueue = multiprocessing.Queue()
## CREATE HANDLERS ###
standardFormat = logging.Formatter(fmt=STD_FORMAT_STR,
datefmt=STD_DATE_FORMAT_STR)
_hndlr = StreamHandler()
_hndlr.setLevel("DEBUG")
_hndlr.setFormatter(standardFormat)
### CREATE AND START QUEUE LISTENER ###
queueListener = QueueListener(loggingQueue,_hndlr,respect_handler_level=True)
queueListener.start()
atexit.register(queueListener.stop) # ensures that queue resources are cleared when dying
return loggingQueue
def setup_mp_device_queued_logging(loggingQueue):
"""
1. Take in the shared mp.Queue that all devices write to
2. Create a QueueHandler that log records will push to
Args:
loggingQueue (_type_): _description_
"""
loggingDictConfig = {
"version": 1,
"disable_existing_loggers": True,
"formatters" : {
},
"handlers": {
"myQueueHandler" : {
"class" : "logging.handlers.QueueHandler",
"queue" : loggingQueue
}
},
"root" : {
"level": "DEBUG",
"handlers" : [
"myQueueHandler"
]
},
}
### CREATE THE QUEUEHANDLER + CONFIGURE LOGGING ###
logging.config.dictConfig(loggingDictConfig)
def someFoo(myQ : multiprocessing.Queue, nm : str):
setup_mp_device_queued_logging(myQ)
logger = logging.getLogger(f"{nm}")
logger.debug(f"Hello from {nm}")
pass
if __name__ == "__main__":
myQ = create_share_logging_queue()
setup_mp_device_queued_logging(myQ)
logger = logging.getLogger("MAIN")
p1 = multiprocessing.Process(target=someFoo, args=(myQ,"CoolProc1"))
p2 = multiprocessing.Process(target=someFoo, args=(myQ,"CoolProc2"))
p1.start()
p2.start()
logger.debug("Hello From Main")
p1.join()
p2.join()
输出:
[DEBUG @ MAIN|test.py|L72|2024-09-05T16:46:42-0500|26284]: Hello From Main
[DEBUG @ CoolProc1|test.py|L60|2024-09-05T16:46:42-0500|11940]: Hello from CoolProc1
[DEBUG @ CoolProc2|test.py|L60|2024-09-05T16:46:42-0500|19016]: Hello from CoolProc2
注意事项:
my_tools.py
),将记录器设置为全局变量并允许它从调用该工具的人那里继承配置的记录器会非常有帮助。这需要您将“禁用记录器”设置为 False 并手动删除您想要的记录器。我发现这个片段很有帮助:VALID_LOGGERS = ['MAIN','CoolProc1','CoolProc2']
disabled_logger_cnt = 0
for log_name in logging.Logger.manager.loggerDict:
if not log_name in VALID_LOGGERS:
log_obj = logging.getLogger(log_name)
log_obj.setLevel(logging.WARNING)
disabled_logger_cnt += 1
因为这看起来像是在 tkinter 应用程序中,所以请明智地管理您的 mp 队列。错误地关闭进程可能会导致挂起,从而导致挫败感,因为它们依赖于共享日志记录队列资源。
查看 mCoding 的日志记录教程。它内容丰富,描述了良好的做法:https://youtu.be/9L77QExPmI0?si=4ggPehbVIzJoegip
=== TLDR ===
不要在进程之间共享记录器。请使用队列将日志消息传递到主记录器。可能会创建一个配置记录器的函数,并且每个进程仅配置和获取记录器一次。