在线程中运行长时间的 Python 计算,并记录到 Qt 窗口,在短时间内崩溃 [英] Running a long Python calculation in a thread, with logging to a Qt window, crashes after a short while
问题描述
我有一个来自单独项目的大模块,我想将其集成到 GUI 中.该模块执行一些需要几分钟的计算,我希望在此期间保持 GUI 响应,并且最好能够随时取消该过程.
I have a large module from a separate project, which I wanted to integrate into a GUI. The module does some calculations that take a couple of minutes, and I want to keep the GUI responsive during this time, and preferrably be able to cancel the process at any time.
最好的解决方案可能是使用信号和线程重写模块,但我想尝试在没有开始的情况下进行.所以我的想法是在一个单独的线程中运行 myLongFunction
.
The best solution would probably be to rewrite the module using signals and threading, but I wanted to try to do it without that to start. So my idea was to run myLongFunction
in a separate thread.
在 GUI 中,我制作了一个文本框(一个 QPlainTextEdit
),我想在其中通过 Python 的日志记录工具显示消息.我还有一个开始"按钮.
In the GUI I have made a text box (a QPlainTextEdit
) where I want to display messages via the logging facilities of Python. I also have a "Start" button.
该程序似乎按预期运行了一段时间,但通常会在 10 秒内崩溃.有时它会立即崩溃,有时需要更长的时间.而且我没有收到任何异常或其他错误,我只是返回到终端提示.下面是一个最小的例子.
The program seems to work as intended for a little while, but it usually crashes within 10 seconds. Sometimes it crashes right away, sometimes it takes a bit longer. And I get no exceptions or other errors, I'm just returned to the terminal prompt. A minimal example is below.
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 分钟后手动停止了它).
The strangest thing is that if I remove the text box (comment out line 51-56 and line 72), the program runs just fine (I stopped it manually after 5 minutes).
知道是什么原因造成的吗?
Any idea what could cause this?
推荐答案
按照提示操作 GM,我制作了一个我认为遵守 Qt 规则的版本.我创建了一个 ThreadLogger(logging.Handler)
类,我将它设置为处理 Worker
线程中的所有日志,并通过插槽和信号将它们发送到主线程.
Following the hint by G.M., I have made a version that I think obeys the rules of Qt. I made a ThreadLogger(logging.Handler)
class, which I set to handle all logs in the Worker
thread, and send them to the main thread via slots and signals.
当我继承 QtCore.QObject
(和 logging.Handler
) 在 ThreadLogger
中,我怀疑是因为我覆盖了 QtCore.Signal.emit() 所以我把信号放在一个单独的 MyLog(QObject)
类中,并在 ThreadLogger
I kept getting the error TypeError: emit() takes 2 positional arguments but 3 were given
when I inherited QtCore.QObject
(and logging.Handler
) in ThreadLogger
, which I suspect was because I was overriding QtCore.Signal.emit() So I put the Signal in a separate MyLog(QObject)
class, and just use an instance of that in 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
发出所有日志
And here is the ThreadLogger(logging.Handler)
class. This just emits all logs via signal
in MyLog
above
# 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%,但至少它不会像以前那样崩溃.
I am not quite sure why yet, but I get all logs from longFunction
in the terminal as well as in the GUI window (but with different formats). I am also not 100% if this is actually thread-safe, and obeys all Qt threading rules, but at least it doesn't crash in the same way as before.
我会把这个答案搁置几天,如果我没有得到更好的答案,或者我的解决方案是错误的,那么就接受它!
I will leave this answer up for a couple of days, and accept it then if I don't get any better answers, or it turns out my solution is wrong!
这篇关于在线程中运行长时间的 Python 计算,并记录到 Qt 窗口,在短时间内崩溃的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!