在线程中运行长时间的 Python 计算,并记录到 Qt 窗口,片刻后崩溃

Posted

技术标签:

【中文标题】在线程中运行长时间的 Python 计算,并记录到 Qt 窗口,片刻后崩溃【英文标题】:Running a long Python calculation in a thread, with logging to a Qt window, crashes after a short while 【发布时间】:2018-09-24 12:28:41 【问题描述】:

我有一个来自单独项目的大型模块,我想将其集成到 GUI 中。该模块进行了一些需要几分钟的计算,我希望在此期间保持 GUI 响应,并且最好能够随时取消该过程。

最好的解决方案可能是使用信号和线程重写模块,但我想尝试在没有开始的情况下这样做。所以我的想法是在单独的线程中运行myLongFunction

在 GUI 中,我创建了一个文本框 (QPlainTextEdit),我想在其中通过 Python 的日志记录工具显示消息。我还有一个“开始”按钮。

该程序似乎可以按预期运行一段时间,但通常会在 10 秒内崩溃。有时它会立即崩溃,有时它需要更长的时间。而且我没有收到任何异常或其他错误,我只是返回到终端提示符。下面是一个最小的示例。

import sys
import time
import logging
from PySide2 import QtWidgets, QtCore
import numpy as np


def longFunction():
    logging.info("Start long running function")
    i = 0
    while True:
        for j in range(10000):
            t = np.arange(256)
            sp = np.fft.fft(np.sin(t))
            freq = np.fft.fftfreq(t.shape[-1])
            sp = sp + freq
        logging.info("%d" % i)
        i += 1

        # I added a sleep here, but it doesn't seem to help
        time.sleep(0.001)


# since I don't really need an event thread, I subclass QThread, as per
# https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
class Worker(QtCore.QThread):
    def __init__(self, parent=None):
        super().__init__(parent)

    def run(self):
        longFunction()


# custom logging handler
class QTextEditLogger(logging.Handler):
    def __init__(self, parent=None):
        super().__init__()
        self.widget = QtWidgets.QPlainTextEdit(parent)
        self.widget.setReadOnly(True)

    def emit(self, record):
        msg = self.format(record)
        self.widget.appendPlainText(msg)
        self.widget.centerCursor()  # scroll to the bottom


class MyWidget(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        logTextBox = QTextEditLogger(self)

        # format what is printed to text box
        logTextBox.setFormatter(
            logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s'))
        logging.getLogger().addHandler(logTextBox)

        # set the logging level
        logging.getLogger().setLevel(logging.DEBUG)

        self.resize(400, 500)

        # start button
        self.startButton = QtWidgets.QPushButton(self)
        self.startButton.setText('Start')

        # connect start button
        self.startButton.clicked.connect(self.start)

        # set up layout
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(logTextBox.widget)
        layout.addWidget(self.startButton)
        self.setLayout(layout)

    def start(self):
        logging.info('Start button pressed')

        self.thread = Worker()

        # regardless of whether the thread finishes or the user terminates it
        # we want to show the notification to the user that it is done
        # and regardless of whether it was terminated or finished by itself
        # the finished signal will go off. So we don't need to catch the
        # terminated one specifically, but we could if we wanted.
        self.thread.finished.connect(self.threadFinished)  # new-style signal

        self.thread.start()

        # we don't want to enable user to start another thread while this one
        # is running so we disable the start button.
        self.startButton.setEnabled(False)

    def threadFinished(self):
        logging.info('Thread finished!')
        self.startButton.setEnabled(True)


app = QtWidgets.QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()

最奇怪的是,如果我删除文本框(注释掉第 51-56 行和第 72 行),程序运行得很好(我在 5 分钟后手动停止了它)。

知道是什么原因造成的吗?

【问题讨论】:

我对您代码的某些方面不太熟悉,但乍一看,我会说您正在尝试直接从您的主 GUI 线程上更新 QTextEditLogger辅助线程(即Worker)。这是不支持的。您最初的想法是使用信号/插槽和队列连接。 我没有想到这一点。不过,我还是不想碰我正在使用的另一个模块。也许有一种方法可以在 Worker 线程中捕获记录器信号,并将它们传递给主线程? 我在下面发布了一个答案,试图避免您指出的问题。不确定这是否是正确的方法,但至少现在不会崩溃。 【参考方案1】:

按照G.M. 的提示,我制作了一个我认为符合Qt 规则的版本。我创建了一个ThreadLogger(logging.Handler) 类,我将其设置为处理Worker 线程中的所有日志,并通过槽和信号将它们发送到主线程。

当我在ThreadLogger 中继承QtCore.QObject(和logging.Handler)时,我一直收到错误TypeError: emit() takes 2 positional arguments but 3 were given,我怀疑这是因为我覆盖了QtCore.Signal.emit() 所以我把信号放在一个单独的 MyLog(QObject) 类,并在 ThreadLogger 中使用该类的一个实例

class MyLog(QtCore.QObject):
    # create a new Signal
    # - have to be a static element
    # - class  has to inherit from QObject to be able to emit signals
    signal = QtCore.Signal(str)

    # not sure if it's necessary to implement this
    def __init__(self):
        super().__init__()

这里是ThreadLogger(logging.Handler) 类。这只是通过上面的MyLog 中的signal 发出所有日志

# custom logging handler that can run in separate thread, and emit all logs
# via signals/slots so they can be used to update the GUI in the main thread
class ThreadLogger(logging.Handler):
    def __init__(self):
        super().__init__()
        self.log = MyLog()

    # logging.Handler.emit() is intended to be implemented by subclasses
    def emit(self, record):
        msg = self.format(record)
        self.log.signal.emit(msg)

完整的代码是

import sys
import logging
import numpy as np
from PySide2 import QtWidgets, QtCore


def longFunction(logger):
    logger.info("Start long running function")
    i = 0
    while True:
        for j in range(10000):
            t = np.arange(256)
            sp = np.fft.fft(np.sin(t))
            freq = np.fft.fftfreq(t.shape[-1])
            sp = sp + freq
        logger.info("%d" % i)
        i += 1


# since I don't really need an event thread, I subclass QThread, as per
# https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
class Worker(QtCore.QThread):
    def __init__(self, parent=None):
        super().__init__(parent)

        ## set up logging
        # __init__ is run in the thread that creates this thread, not in the
        # new thread. But logging is thread-safe, so I don't think it matters

        # create logger for this class
        self.logger = logging.getLogger("Worker")

        # set up log handler
        self.logHandler = ThreadLogger()
        self.logHandler.setFormatter(
            logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s'))
        self.logger.addHandler(self.logHandler)

        # set the logging level
        self.logger.setLevel(logging.DEBUG)

    def run(self):
        longFunction(self.logger)


class MyLog(QtCore.QObject):
    # create a new Signal
    # - have to be a static element
    # - class  has to inherit from QObject to be able to emit signals
    signal = QtCore.Signal(str)

    # not sure if it's necessary to implement this
    def __init__(self):
        super().__init__()


# custom logging handler that can run in separate thread, and emit all logs
# via signals/slots so they can be used to update the GUI in the main thread
class ThreadLogger(logging.Handler):
    def __init__(self):
        super().__init__()
        self.log = MyLog()

    # logging.Handler.emit() is intended to be implemented by subclasses
    def emit(self, record):
        msg = self.format(record)
        self.log.signal.emit(msg)


class MyWidget(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        # read-only text box
        self.logTextBox = QtWidgets.QPlainTextEdit(self)
        self.logTextBox.setReadOnly(True)

        self.resize(400, 500)

        # start button
        self.startButton = QtWidgets.QPushButton(self)
        self.startButton.setText('Start')

        # connect start button
        self.startButton.clicked.connect(self.start)

        # set up layout
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.logTextBox)
        layout.addWidget(self.startButton)
        self.setLayout(layout)

    def start(self):
        self.thread = Worker()
        self.thread.finished.connect(self.threadFinished)
        self.thread.start()

        # we don't want to enable user to start another thread while this one
        # is running so we disable the start button.
        self.startButton.setEnabled(False)

        # connect logger
        self.thread.logHandler.log.signal.connect(self.write_log)

    def threadFinished(self):
        self.startButton.setEnabled(True)

    # define a new Slot, that receives a string
    @QtCore.Slot(str)
    def write_log(self, log_text):
        self.logTextBox.appendPlainText(log_text)
        self.logTextBox.centerCursor()  # scroll to the bottom


app = QtWidgets.QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()

我还不太清楚为什么,但我从终端和 GUI 窗口中的 longFunction 获取所有日志(但格式不同)。如果这实际上是线程安全的,并且遵守所有 Qt 线程规则,我也不是 100%,但至少它不会像以前那样崩溃。

我将把这个答案搁置几天,如果我没有得到更好的答案,或者结果证明我的解决方案是错误的,我会接受它!

【讨论】:

以上是关于在线程中运行长时间的 Python 计算,并记录到 Qt 窗口,片刻后崩溃的主要内容,如果未能解决你的问题,请参考以下文章

顺序并在一个线程中运行时差c ++ [关闭]

中止正在运行长查询的线程

使用 Httpclient 进行长轮询

xp批处理文件中的行长限制?

在 VS Code 中运行时 Python 跳过函数

通过将长时间运行的任务拆分为单独的进程来提高程序性能