在一个线程中运行一个长Python计算,并记录到一个Qt窗口,很快就会崩溃

在一个线程中运行一个长Python计算,并记录到一个Qt窗口,很快就会崩溃,python,qthread,pyside2,Python,Qthread,Pyside2,我有一个单独项目中的大模块,我想将其集成到GUI中。该模块执行一些需要几分钟时间的计算,我希望在这段时间内保持GUI的响应性,最好能够随时取消该过程 最好的解决方案可能是使用信号和线程重写模块,但我想尝试在不启动信号和线程的情况下完成。所以我的想法是在一个单独的线程中运行MyLong函数 在GUI中,我制作了一个文本框QPlainTextEdit,在这里我希望通过Python的日志记录功能显示消息。我还有一个开始按钮 该程序似乎在一段时间内按预期运行,但通常在10秒内崩溃。有时它会立即崩溃,有时

我有一个单独项目中的大模块,我想将其集成到GUI中。该模块执行一些需要几分钟时间的计算,我希望在这段时间内保持GUI的响应性,最好能够随时取消该过程

最好的解决方案可能是使用信号和线程重写模块,但我想尝试在不启动信号和线程的情况下完成。所以我的想法是在一个单独的线程中运行MyLong函数

在GUI中,我制作了一个文本框QPlainTextEdit,在这里我希望通过Python的日志记录功能显示消息。我还有一个开始按钮

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

导入系统 导入时间 导入日志记录 从PySide2导入QtWidgets、QtCore 将numpy作为np导入 def长功能: logging.infoStart长时间运行函数 i=0 尽管如此: 对于10000范围内的j: t=np.arange256 sp=np.fft.fftnp.sint freq=np.fft.fft需求形状[-1] sp=sp+频率 日志记录。信息%d%i i+=1 我在这里加了一个睡眠,但似乎没有帮助 时间0.001 因为我实际上不需要事件线程,所以我将QThread子类化,如下所示 https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html 类WorkerQtCore.QThread: def u_init__self,父项=无: 超级。初始父级 def runself: 长函数 自定义日志处理程序 类QTextEditLoggerlogging.Handler: def u_init__self,父项=无: 超级__ self.widget=qtwidts.QPlainTextEditparent self.widget.setReadOnlyTrue 定义自我,记录: msg=self.formatrecord self.widget.appendPlainTextmsg self.widget.centerCursor滚动到底部 类MyWidgetQtWidgets.QDialog: def u_init__self,父项=无: 超级。初始父级 logTextBox=QTextEditLoggerself 格式化打印到文本框的内容 logTextBox.setFormatter 日志记录。格式化程序“%asctimes-%levelnames-%threadNames-%messages” logging.getLogger.addHandlerlogTextBox 设置日志记录级别 logging.getLogger.setLevellogging.DEBUG self.resize400500 开始按钮 self.startButton=qtwidts.QPushButtonself self.startButton.setText'Start' 连接启动按钮 self.startButton.clicked.connectself.start 设置布局 layout=qtwidts.QVBoxLayout layout.addWidgetlogTextBox.widget layout.addWidgetself.startButton self.setLayoutlayout def startself: logging.info“按下启动按钮” self.thread=Worker 不管线程是结束还是用户终止它 我们希望向用户显示已完成的通知 不管它是被终止还是自行结束 完成的信号将熄灭。所以我们不需要抓住机会 专门终止了一个,但如果我们愿意,我们可以。 self.thread.finished.connectself.threadFinished新型信号 self.thread.start 我们不想让用户在这个线程运行时启动另一个线程 正在运行,因此我们禁用“开始”按钮。 self.startButton.setEnabledFalse def螺纹精加工工具: logging.info“线程已完成!” self.startButton.setEnabledTrue app=qtwidts.QApplicationsys.argv w=MyWidget w、 展示 app.exec_ 最奇怪的是,如果我删除第51-56行和第72行的文本框注释,程序运行正常,我会在5分钟后手动停止它

你知道这是什么原因吗?

根据的提示,我制作了一个我认为符合Qt规则的版本。我创建了一个ThreadLoggerlogging.Handler类,我将其设置为处理工作线程中的所有日志,并通过插槽和信号将它们发送到主线程

我一直收到错误TypeError:emit接受2个位置参数,但3个是在ThreadLogger中继承QtCore.QObject和logging.Handler时给出的,我怀疑这是因为我重写了QtCore.Signal.emit,所以我将该信号放在一个单独的MyLogQObject类中,并在ThreadLogger中使用该类的一个实例

这是ThreadLoggerlogging.Handler类。这只是通过上面MyLog中的信号发出所有日志

完整的代码是

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%,但至少它不是 不要像以前那样崩溃


我会把这个答案保留几天,如果我没有得到更好的答案,或者我的解决方案是错误的,我会接受它

我对代码的某些方面不太熟悉,但乍一看,我想说您正试图从辅助线程(即Worker)直接更新位于主GUI线程上的QTextEditLogger。这是不受支持的。您最初的想法是将信号/插槽与排队连接一起使用,我没有想到这一点。我还是不想碰我正在使用的另一个模块。也许有一种方法可以捕获工作线程中的记录器信号,并将其传递给主线程?我在下面发布了一个答案,试图避免您指出的问题。不确定这是不是正确的方式,但至少它现在没有崩溃。
# 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_()