Python多进程 - 将子进程日志发送到运行在父进程中的GUI

4
我正在为我编写的一些分析代码构建一个界面,该界面执行一些SQL并处理查询结果。在这个分析代码中,有许多事件都被记录下来,我希望将这些日志暴露给用户。由于分析代码运行时间较长,并且我不想UI被阻塞,因此到目前为止,我通过将分析函数放入自己的线程中来实现这一点。
现在是一个简化的示例(完整脚本):
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.Poolapply_async 可以很容易地获得相同的功能,以非阻塞UI的方式执行分析代码。

例如,将上面的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_taskMainwindow.start。当我运行main.py时,会得到以下结果:

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

这似乎是这个SO问题中所描述的解决方案

使用QueueQueueHandler的这个想法作为解决方案与@eyllanesc的原始解决方案相似

2个回答

2
信号不能在进程之间传输数据,因此在这种情况下必须使用管道(Pipe)来发射信号:
# 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

# ...

@user3014097,在以下链接中,您将找到我的测试代码:https://gist.github.com/eyllanesc/72a67e76350691012580f2207f1dd12b,请尝试并告诉我是否有效。 - eyllanesc
@user3014097 我已经在Linux上使用PySide2 5.11和Python 3.7进行了测试,如图所示(https://user-images.githubusercontent.com/4896200/48495203-7aee9b80-e7fd-11e8-8d15-0f529f517762.png),它可以正常工作。 - eyllanesc
@user3014097,链接问题请参考此链接:https://dev59.com/JoPba4cB1Zd3GeqPzO8Q。 - eyllanesc
所以,我看到了与此处描述的相同的行为(请参见我的编辑): https://dev59.com/nJLea4cB1Zd3GeqP8Oap但是,规定的修复方法不是你的代码正在做的吗? - dan_g
1
我会将这个标记为已解决,因为你的解决方案显然可行。考虑到平台的限制,在 Windows 上如何让它工作,我仍然不确定,但至少这是一个开始的地方。 - dan_g
显示剩余5条评论

2

如果有人在将来遇到这个问题,使用 QueueHandlerQueueListener 可以解决在 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_())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接