Я пытаюсь создать небольшое приложение с графическим интерфейсом пользователя с регистратором, которое в какой-то момент будет выполнять трудоемкие манипуляции с несколькими наборами данных (до нескольких сотен). Естественно, я хотел использовать многопроцессорность, чтобы немного ускорить процесс. Я следовал примеру, приведенному в Поваренной книге ведения журнала (второй пример) в документации Python, и пытался понять, как внедрить его в свой код. В этом урезанном минимальном примере нажатие кнопки сборки должно просто записать несколько сообщений. Проблема, которая, надеюсь, очевидна для тех, кто более сведущ в рассматриваемых темах, заключается в том, что она не работает должным образом. Приложение выводит на консоль только 3 из 5 сообщений, а в файл журнала добавляется ровно ноль.
Очевидно, я ожидал, что все 5 сообщений будут зарегистрированы с помощью экземпляра logger
, созданного в gui.py
.
Я пробовал объединять методы, перемещать методы из класса в функции уровня модуля, создавать Queue
/loggers в разных местах и передавать первый экземпляр logger в качестве аргумента. Все, что я пробовал до этого момента, либо приводило к тем же результатам, либо выдавало ошибку 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
для средства ведения журнала существовал только для того, чтобы отложить настройку до тех пор, пока не будет создан графический интерфейс, чтобы он имел доступ к виджету «Текст» для записи в пользовательский обработчик.
Совместное использование объекта журнала между процессами может быстро стать странным.
Я бы рекомендовал использовать класс 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://thewikihow.com/video_9L77QExPmI0?si=4ggPehbVIzJoegip
=== TLDR ===
Не делитесь журналами между процессами. Используйте очереди для передачи сообщений журнала в главный регистратор. Вероятно, создайте функцию для настройки регистратора и настраивайте и получайте регистраторы только один раз для каждого процесса.
Мне потребовалось всего 5 минут, чтобы заставить этот шаблон работать с моим примером кода, включая сохранение регистратора в отдельном потоке. По какой-то причине мой мозг застрял на идее поделиться одним регистратором. Я никогда не думал попробовать включить регистратор в каждый процесс. Самая забавная часть этого эпизода для меня заключается в том, что код, которым вы поделились, удивительно похож на мою первоначальную попытку (с использованием QueueListener), прежде чем я позволил приведенному мной примеру кода направить меня по другому пути. Спасибо за это.