PyQt, QThread, GIL, графический интерфейс

У меня есть графический интерфейс и программная логика, написанная на Python. Я запрашиваю информацию из Интернета, вызывая urllib.requests (и так далее) очень часто, и это вызывает проблему, когда графический интерфейс не отвечает, но эти вызовы завернуты в QThread. Я думаю, что это происходит из-за GIL. Но как, если я могу использовать QThread в приложении PyQt, какая польза от него в PyQt, если я не могу заставить код работать асинхронно?

--Код--

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

И место, где я его использую:

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()

В моем случае _get_servers() и _get_user_info() вызываются по порядку, но я хочу выполнять их одновременно.


person Victor Polevoy    schedule 08.09.2014    source источник
comment
Вы правильно начинаете поток, например. используя worker_thread.start()? Потому что, если вы сделаете worker_thread.run(), он запустится. Только не в другой теме.   -  person Fenikso    schedule 08.09.2014
comment
Именно worker_thread.start(). Подождите, я выложу код.   -  person Victor Polevoy    schedule 08.09.2014
comment
Интересное использование декораторов... Не могли бы вы создать небольшой исполняемый пример с кнопкой, запускающей сон в другом потоке? Или я могу ответить на ваш вопрос рабочим примером потоковой передачи, но я не уверен, ответит ли он на ваш вопрос, как написано.   -  person Fenikso    schedule 08.09.2014
comment
Я согласен с @Fenikso, определенно интересное использование декораторов. Я думаю, ваша проблема может быть связана с тем, что wrapper не возвращает рабочего, поэтому он не существует вне этой функции.   -  person user3419537    schedule 08.09.2014
comment
Пробовал вернуть worker и True из обертки - безрезультатно.   -  person Victor Polevoy    schedule 08.09.2014
comment
Я думаю, что ваша проблема заключается в обертке, а не в самом потоке. Выглядит хорошо, вероятно, есть ловушка на основе обертки.   -  person Fenikso    schedule 08.09.2014
comment
Недостаточно просто вернуть его из wrapper. В конечном счете вам нужно, чтобы работник существовал вне функций, которые вы декорируете, иначе вы столкнетесь с той же проблемой, когда эти функции вернутся. Я думаю, что вы слишком усложняете для себя, используя декораторы.   -  person user3419537    schedule 08.09.2014
comment
Я думаю, что вы правы насчет чрезмерного усложнения, но если у меня небольшой объем кода и нужно получить несколько очень маленьких вещей для работы, параллельные декораторы — лучшее решение, больше — если у вас мало времени. Я исправил проблему, позвольте мне проверить ее еще раз, и я опубликую решение.   -  person Victor Polevoy    schedule 08.09.2014
comment
Я только что попробовал ваши декораторы в своем тестовом коде. Это не работает правильно. Я плохо разбираюсь в декораторах, поэтому не могу помочь.   -  person Fenikso    schedule 08.09.2014


Ответы (2)


Я думаю, что вы слишком усложняете использование декораторов. Вы можете легко обернуть свой код в новый поток, используя около 3-4 строк кода установки. Также я не думаю, что вам следует вызывать готовый слот напрямую из другого потока. Вы должны использовать подключенный сигнал, чтобы активировать его.

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_())
person Fenikso    schedule 08.09.2014
comment
Я исправил проблему с декоратором, он не работает, но да, использование декораторов может быть немного сложным. Другая проблема - обновление графического интерфейса (который не был опубликован в моем коде) решается путем подключения к сигналу finished потоков, которые я создал декоратором. Я обновлю свой вопрос решением для других, которые хотят знать, как заставить методы работать одновременно с помощью декораторов. - person Victor Polevoy; 08.09.2014
comment
Я не вижу сигнала в вашем коде. Я вижу только self._finished_slot(). - person Fenikso; 08.09.2014
comment
@VictorPolevoy Не обновляйте вопрос с исправлением. Вместо этого напишите ответ. - person Fenikso; 08.09.2014

Хотя решение уже предложено Fenikso, может быть также интересно, как решить проблему с помощью декораторов.

Я исправил свой qtthreaddecorator.py следующим образом:

from PyQt4 import QtCore


class Worker(QtCore.QThread):
    threads = []

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

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

    def run(self):
        Worker.threads.append(self.currentThreadId())            
        self._function(*self._args, **self._kwargs)
        self.emit(QtCore.SIGNAL('finished()'))
        Worker.threads.remove(self.currentThreadId())


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

            def on_finish():
                worker.quit()

            worker.finished.connect(on_finish)
            worker.start()

            return worker
        return wrapper
    return decorator

И в коде, который использует этот декоратор:

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()
        def _get_servers():
            self._get_my_servers()
        @qtthreaddecorator.qt_thread_decorator()
        def _get_user_info():
            self._get_user_info()

        _get_servers().finished.connect(self._fill_servers)
        _get_user_info().finished.connect(self._fill_user_info)
person Victor Polevoy    schedule 08.09.2014
comment
Но я все еще думаю о том, когда worker.quit() действительно уничтожит поток - после уведомления каждого из finished()-подписчиков или нет? Может ли это вызвать проблему? - person Victor Polevoy; 08.09.2014
comment
Я бы рекомендовал использовать новую нотацию для сигналов. Я думаю, что старый удален в PyQt5. - person Fenikso; 08.09.2014