Skip to main content
 首页 » 编程设计

python multiprocessing之将子进程日志记录发送到在父进程中运行的 GUI

2024年09月03日31lexus

我正在我编写的一些分析代码之上构建一个接口(interface),该接口(interface)执行一些 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.Pool 和 apply_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_task 和 Mainwindow.start。当我运行 main.py 时,我得到:

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

这似乎是 this SO question 中描述的内容

这种使用 Queue 和 QueueHandler 作为解决方案的想法似乎类似于@eyllanesc 的原始解决方案

请您参考如下方法:

信号不在进程之间传输数据,因此对于这种情况,必须使用管道然后发出信号:

# 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 
 
# ...