Issue
I am attempting to make a simple factory function to wrap Python functions in QThread
to prevent blocking the main application thread when performing long-running noncritical operations in the background (for example caching data).
I have attempted to follow the popular blog post that describes the actual intended usage of QThread
without subclassing it and overriding run
. I have been searching for the solutions to my problems for well over half of the day and can't seem to get any sort of thing working, no matter what I try and how. I'm tempted to just do this the "wrong" way and subclass QThread
. The code for the wrapper follows.
from qtpy.QtCore import Slot, Signal, QThread, QObject
class Worker(QObject):
finished = Signal()
def __init__(self, target, *args, parent=None, **kwargs):
super().__init__(parent)
self.__target = target
self.__args = args
self.__kwargs = kwargs
def run(self):
self.__target(*self.__args, **self.__kwargs)
self.finished.emit()
def create_thread(target, *args, parent=None, **kwargs):
thread = QThread(parent)
worker = Worker(target, *args, **kwargs, parent=parent)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
return thread
When using the make_thread
function passing the target
without setting the parent
to the instance of the QMainWindow
, the target
function executes and blocks, but then crashes silently after finished
is emitted. Any print
calls within the target
function also fail to print anything to stdout
.
Process finished with exit code -1073741819 (0xC0000005)
If the parent
is set, the target
runs and does print to stdout
but also blocks the main thread and prevents widgets from becoming visible until finished
is emitted, leaving the main window hung and blank for the duration.
The example interface I am using simply has a QLabel
for an example widget that is intended to show immediately while the target
executes in the background. It does not show until the target
completes, and then only if parent
of the QThread
is specified. Otherwise, the program just crashes (after exactly five seconds, as is shown in the example below). None of this code works in either PySide2
or PyQt5
.
import sys
import time
from qtpy.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxLayout
import modpack_builder.gui.helpers as helpers
class ExampleMainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setCentralWidget(QWidget(self))
self.centralWidget().setLayout(QVBoxLayout(self.centralWidget()))
self.label = QLabel("If you see this immediately, the thread did not block.", self.centralWidget())
self.centralWidget().layout().addWidget(self.label)
self.test_thread = helpers.create_thread(self.long_running_task)
self.test_thread.start()
@staticmethod
def long_running_task():
print("Task started.")
time.sleep(5)
print("Task finished.")
if __name__ == "__main__":
app = QApplication(list())
window = ExampleMainWindow()
window.show()
sys.exit(app.exec_())
If there are too many tags on this question or if they are redundant please let me know. I can never tell for sure which ones to use.
Solution
You should consider the following:
If a QObject has a parent then its life cycle depends on the parent, otherwise it depends on python which is handled by the concept of scope.
A QObject belongs to the same thread as the parent.
It is not thread-safe that the methods of a QObject are executed in a different thread than the one they belong to, except for the signals.
Considering the above "worker" is a QObject that does not have a parent so python will handle its memory and in this case it is a local variable that will be destroyed after executing the create_thread function. Possible solutions are passing it a QObject as a parent, making it a global variable, making it an attribute of another object that has a larger life cycle, etc.
On the other hand, worker and thread cannot have the same QObject as their parent, since by definition they live in different threads, remember that QThread is not a thread but a thread handler and belongs to the thread where it was created and not to the thread it manages.
If you are going to invoke a method of a QObject from another thread through a signal then use the @Slot decoration.
It is not necessary to use "emit" in the connection between signals.
Considering the above, you can make worker an attribute of the QThread.
import sys
import time
from qtpy.QtCore import Slot, Signal, QThread, QObject
from qtpy.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxLayout
class Worker(QObject):
start = Signal()
finished = Signal()
def __init__(self, target, *args, parent=None, **kwargs):
super().__init__(parent)
self.__target = target
self.__args = args
self.__kwargs = kwargs
self.start.connect(self.run)
@Slot()
def run(self):
self.__target(*self.__args, **self.__kwargs)
self.finished.emit()
def create_thread(target, *args, parent=None, **kwargs):
thread = QThread(parent)
worker = Worker(target, *args, **kwargs)
worker.moveToThread(thread)
thread.started.connect(worker.start)
# or
# thread.started.connect(worker.run)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.worker = worker
return thread
class ExampleMainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setCentralWidget(QWidget(self))
self.centralWidget().setLayout(QVBoxLayout(self.centralWidget()))
self.label = QLabel("If you see this immediately, the thread did not block.",)
self.centralWidget().layout().addWidget(self.label)
self.test_thread = create_thread(self.long_running_task)
self.test_thread.start()
@staticmethod
def long_running_task():
print("Task started.")
time.sleep(5)
print("Task finished.")
if __name__ == "__main__":
app = QApplication(list())
window = ExampleMainWindow()
window.show()
sys.exit(app.exec_())
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.