многопроцессорность Python - отправка журнала дочернего процесса в графический интерфейс, запущенный в parent - PullRequest
0 голосов
/ 13 ноября 2018

Я строю интерфейс поверх написанного мной кода анализа, который выполняет SQL и обрабатывает результаты запроса.В этом аналитическом коде есть записи о ряде событий, которые я хотел бы представить пользователю.Поскольку код анализа довольно длительный и потому что я не хочу, чтобы пользовательский интерфейс блокировался, до сих пор я делал это, помещая функцию анализа в свой собственный поток.

Упрощенный пример того, что у меня сейчас (полный скрипт):

import sys
import time
import logging
from PySide2 import QtCore, QtWidgets

def long_task():
    logging.info('Starting long task')
    time.sleep(3) # this would be replaced with a real task
    logging.info('Long task complete')

class LogEmitter(QtCore.QObject):
    sigLog = QtCore.Signal(str)

class LogHandler(logging.Handler):
    def __init__(self):
        super().__init__()
        self.emitter = LogEmitter()
    def emit(self, record):
        msg = self.format(record)
        self.emitter.sigLog.emit(msg)

class LogDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        log_txt = QtWidgets.QPlainTextEdit(self)
        log_txt.setReadOnly(True)
        layout = QtWidgets.QHBoxLayout(self)
        layout.addWidget(log_txt)
        self.setWindowTitle('Event Log')
        handler = LogHandler()
        handler.emitter.sigLog.connect(log_txt.appendPlainText)
        logger = logging.getLogger()
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

class Worker(QtCore.QThread):
    results = QtCore.Signal(object)

    def __init__(self, func, *args, **kwargs):
        super().__init__()
        self.func = func
        self.args = args
        self.kwargs = kwargs

    def run(self):
        results = self.func(*self.args, **self.kwargs)
        self.results.emit(results)

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()
        widget = QtWidgets.QWidget()
        layout = QtWidgets.QHBoxLayout(widget)
        start_btn = QtWidgets.QPushButton('Start')
        start_btn.clicked.connect(self.start)
        layout.addWidget(start_btn)
        self.setCentralWidget(widget)

        self.log_dialog = LogDialog()
        self.worker = None

    def start(self):
        if not self.worker:
            self.log_dialog.show()
            logging.info('Run Starting')
            self.worker = Worker(long_task)
            self.worker.results.connect(self.handle_result)
            self.worker.start()

    def handle_result(self, result=None):
        logging.info('Result received')
        self.worker = None

if __name__ == '__main__':
    app = QtWidgets.QApplication()
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())

Это прекрасно работает, за исключением того, что я должен иметь возможность позволить пользователю остановить выполнение кода анализа,Все, что я прочитал, указывает на то, что нет способа красиво прервать потоки, поэтому использование библиотеки multiprocessing кажется правильным (нет способа переписать код анализа для периодического опроса, так как большинствовремени тратится только на ожидание запросов, чтобы вернуть результаты).Достаточно просто получить те же функциональные возможности с точки зрения выполнения кода анализа способом, который не блокирует пользовательский интерфейс с помощью multiprocessing.Pool и apply_async.

Например, заменив MainWindow сверху на:

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()
        widget = QtWidgets.QWidget()
        layout = QtWidgets.QHBoxLayout(widget)
        start_btn = QtWidgets.QPushButton('Start')
        start_btn.clicked.connect(self.start)
        layout.addWidget(start_btn)
        self.setCentralWidget(widget)

        self.log_dialog = LogDialog()
        self.pool = multiprocessing.Pool()
        self.running = False

    def start(self):
        if not self.running:
            self.log_dialog.show()
            logging.info('Run Starting')
            self.pool.apply_async(long_task, callback=self.handle_result)

    def handle_result(self, result=None):
        logging.info('Result received')
        self.running = False

Но я не могу понять, как мне поступить, извлекая выходные данные регистрации из дочернего процесса и передавая ихродитель, чтобы обновить диалог журнала.Я прочитал почти каждый вопрос SO по этому вопросу, а также примеры поваренной книги о том, как обрабатывать запись в один файл журнала из нескольких процессов, но я не могу понять, как адаптировать эти идеи к тому, что я делаю ».Я пытаюсь сделать здесь.

Редактировать

Итак, пытаясь выяснить, что может происходить, почему я вижу поведение, отличное от @eyllanesc, я добавил:

logger = logging.getLogger()
print(f'In Func: {logger} at {id(logger)}')

и

logger = logging.getLogger()
print(f'In Main: {logger} at {id(logger)}')

до long_task и Mainwindow.start соответственно.Когда я запускаю main.py, я получаю:

In Main: <RootLogger root (INFO)> at 2716746681984
In Func: <RootLogger root (WARNING)> at 1918342302352

, что похоже на то, что было описано в этом SO вопросе

Эта идея использования Queue иQueueHandler хотя решение похоже на оригинальное решение @ eyllanesc

Ответы [ 2 ]

0 голосов
/ 16 ноября 2018

В случае, если кто-нибудь придет к этому в будущем, использование QueueHandler и QueueListener приведет к решению, которое работает и в Windows.Заимствовал сильно от этот ответ на аналогичный вопрос:

import logging
import sys
import time
import multiprocessing
from logging.handlers import QueueHandler, QueueListener
from PySide2 import QtWidgets, QtCore

def long_task():
    logging.info('Starting long task')
    time.sleep(3) # this would be replaced with a real task
    logging.info('Long task complete')

def worker_init(q):
    qh = QueueHandler(q)
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    logger.addHandler(qh)

class LogEmitter(QtCore.QObject):
    sigLog = QtCore.Signal(str)

class LogHandler(logging.Handler):
    def __init__(self):
        super().__init__()
        self.emitter = LogEmitter()
    def emit(self, record):
        msg = self.format(record)
        self.emitter.sigLog.emit(msg)

class LogDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.log_txt = QtWidgets.QPlainTextEdit(self)
        self.log_txt.setReadOnly(True)
        layout = QtWidgets.QHBoxLayout(self)
        layout.addWidget(self.log_txt)
        self.setWindowTitle('Event Log')

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()
        widget = QtWidgets.QWidget()
        layout = QtWidgets.QHBoxLayout(widget)
        start_btn = QtWidgets.QPushButton('Start')
        start_btn.clicked.connect(self.start)
        layout.addWidget(start_btn)
        self.setCentralWidget(widget)

        self.log_dialog = LogDialog()
        self.running = False

        # sets up handler that will be used by QueueListener
        # which will update the LogDialoag
        handler = LogHandler()
        handler.emitter.sigLog.connect(self.log_dialog.log_txt.appendPlainText)

        self.q = multiprocessing.Queue()
        self.ql = QueueListener(self.q, handler)
        self.ql.start()

        # main process should also log to a QueueHandler
        self.main_log = logging.getLogger('main')
        self.main_log.propagate = False
        self.main_log.setLevel(logging.INFO)
        self.main_log.addHandler(QueueHandler(self.q))

        self.pool = multiprocessing.Pool(1, worker_init, [self.q])

    def start(self):
        if not self.running:
            self.log_dialog.show()
            self.main_log.info('Run Starting')
            self.pool.apply_async(long_task, callback=self.handle_result)

    def handle_result(self, result=None):
        time.sleep(2)
        self.main_log.info('Result received')
        self.running = False

    def closeEvent(self, _):
        self.ql.stop()

if __name__ == '__main__':
    app = QtWidgets.QApplication()
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())
0 голосов
/ 14 ноября 2018

Сигналы не передают данные между процессами, поэтому для этого случая необходимо использовать канал и затем излучать сигнал:

# other imports
import threading
# ...

class LogHandler(logging.Handler):
    def __init__(self):
        super().__init__()
        self.r, self.w = multiprocessing.Pipe()
        self.emitter = LogEmitter()
        threading.Thread(target=self.listen, daemon=True).start()

    def emit(self, record):
        msg = self.format(record)
        self.w.send(msg)

    def listen(self):
        while True:
            try:
                msg = self.r.recv()
                self.emitter.sigLog.emit(msg)
            except EOFError:
                break

# ...
...