Search code examples
pythonpyqtqthreadgil

PyQt, QThread, GIL, GUI


I have GUI and program logic written in Python. I am requesting information from web by calling urllib.requests (and so on) very often and this cause a problem when GUI is unresponsive but this calls are wrapped with QThread. I think that happens because of GIL. But how when I can use QThread in PyQt application, what use of it in PyQt if I can't make code to work asynchronously?

--The code--

qtthreaddecorator.py:

from PyQt4 import QtCore

class Worker(QtCore.QThread):
    def __init__(self, thread_name, finished_slot, function, *args, **kwargs):
        QtCore.QThread.__init__(self)

        self._thread_name = thread_name
        self._function = function
        self._args = args
        self._kwargs = kwargs

        self._finished_slot = finished_slot

    def run(self):
        self._function(*self._args, **self._kwargs)

        self._finished_slot()

        return

def qt_thread_decorator(slot):
    def decorator(function):
        def wrapper(*args, **kwargs):
            worker = Worker(function.__name__, slot, function, *args, **kwargs)
            worker.start()

            return
        return wrapper
    return decorator

And the place where I am using it:

import qtthreaddecorator

class MainWindow(QtGui.QMainWindow, form_class):

def __init__(self, parent=None):
    QtGui.QMainWindow.__init__(self, parent)
    self.setupUi(self)

    self.init()

def init(self):
    @qtthreaddecorator.qt_thread_decorator(self._fill_servers)
    def _get_servers():
        self._get_my_servers()
    @qtthreaddecorator.qt_thread_decorator(self._fill_user_info)
    def _get_user_info():
        self._get_user_info()

    _get_servers()
    _get_user_info()

In my case, _get_servers() and _get_user_info() calls in order but I want to execute them concurrently.


Solution

  • I think you are over-complicating with use of decorators. You can easily wrap your code in new thread using about 3-4 lines of setup code. Also I do not think you should call your finished slot directly from another thread. You should use a connected signal to activate it.

    import sys
    from time import sleep
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    
    class Signals(QObject):
        update = pyqtSignal(int)
        enable_button = pyqtSignal(bool)
    
    class Window(QWidget):
        def __init__(self, *args, **kwargs):
            QWidget.__init__(self, *args, **kwargs)
    
            self.button = QPushButton("Run", self)
            self.button.clicked.connect(self.onButton)
    
            self.progress = QProgressBar(self)
            self.progress.setTextVisible(False)
    
            self.layout = QVBoxLayout()
            self.layout.setContentsMargins(5, 5, 5, 5)
            self.layout.addWidget(self.button)
            self.layout.addWidget(self.progress)
            self.layout.addStretch()
    
            self.worker_thread = QThread()
            self.worker_thread.run = self.worker
            self.worker_thread.should_close = False
    
            self.signals = Signals()
            self.signals.update.connect(self.progress.setValue)
            self.signals.enable_button.connect(self.button.setEnabled)
    
            self.setLayout(self.layout)
            self.show()
            self.resize(self.size().width(), 0)
    
        # Override
        def closeEvent(self, e):
            self.worker_thread.should_close = True
            self.worker_thread.wait()
    
        @pyqtSlot()
        def onButton(self):
            self.button.setDisabled(True)
            self.worker_thread.start()
    
        # Worker thread, no direct GUI updates!
        def worker(self):
            for i in range(101):
                if self.worker_thread.should_close:
                    break
                self.signals.update.emit(i)
                sleep(0.1)
            self.signals.enable_button.emit(True)
    
    app = QApplication(sys.argv)
    win = Window()
    sys.exit(app.exec_())