Как отслеживать прогресс потока в Python, не замораживая PyQt GUI? - PullRequest
19 голосов
/ 20 февраля 2009

Вопросы:

  1. Какова лучшая практика для отслеживание протектора прогресс без блокировки графического интерфейса («Не отвечает»)?
  2. Вообще, каковы лучшие практики для потоки, как это относится к GUI развитие?

Справочная информация:

  • У меня есть PyQt GUI для Windows.
  • Используется для обработки наборов HTML документы.
  • Это занимает от трех секунд до трех часов, чтобы обработать набор документы.
  • Я хочу иметь возможность обрабатывать несколько комплектов одновременно.
  • Я не хочу, чтобы графический интерфейс блокировался.
  • Я смотрю на модуль потоков чтобы достичь этого.
  • Я относительно новичок в потоках.
  • В GUI есть один индикатор выполнения.
  • Я хочу, чтобы он отображал прогресс выбранная тема.
  • Отображение результатов выбранного нить, если она закончена.
  • Я использую Python 2.5.

Моя идея: Пусть потоки испускают QtSignal при обновлении прогресса, который запускает некоторую функцию, которая обновляет индикатор выполнения. Также подайте сигнал о завершении обработки, чтобы можно было отобразить результаты.

#NOTE: this is example code for my idea, you do not have
#      to read this to answer the question(s).

import threading
from PyQt4 import QtCore, QtGui
import re
import copy

class ProcessingThread(threading.Thread, QtCore.QObject):

    __pyqtSignals__ = ( "progressUpdated(str)",
                        "resultsReady(str)")

    def __init__(self, docs):
        self.docs = docs
        self.progress = 0   #int between 0 and 100
        self.results = []
        threading.Thread.__init__(self)

    def getResults(self):
        return copy.deepcopy(self.results)

    def run(self):
        num_docs = len(self.docs) - 1
        for i, doc in enumerate(self.docs):
            processed_doc = self.processDoc(doc)
            self.results.append(processed_doc)
            new_progress = int((float(i)/num_docs)*100)

            #emit signal only if progress has changed
            if self.progress != new_progress:
                self.emit(QtCore.SIGNAL("progressUpdated(str)"), self.getName())
            self.progress = new_progress
            if self.progress == 100:
                self.emit(QtCore.SIGNAL("resultsReady(str)"), self.getName())

    def processDoc(self, doc):
        ''' this is tivial for shortness sake '''
        return re.findall('<a [^>]*>.*?</a>', doc)


class GuiApp(QtGui.QMainWindow):

    def __init__(self):
        self.processing_threads = {}  #{'thread_name': Thread(processing_thread)}
        self.progress_object = {}     #{'thread_name': int(thread_progress)}
        self.results_object = {}      #{'thread_name': []}
        self.selected_thread = ''     #'thread_name'

    def processDocs(self, docs):
        #create new thread
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()

        #add thread to dict of threads
        self.processing_threads[thread_name] = p_thread

        #init progress_object for this thread
        self.progress_object[thread_name] = p_thread.progress  

        #connect thread signals to GuiApp functions
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('progressUpdated(str)'), self.updateProgressObject(thread_name))
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('resultsReady(str)'), self.updateResultsObject(thread_name))

    def updateProgressObject(self, thread_name):
        #update progress_object for all threads
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress

        #update progress bar for selected thread
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])

    def updateResultsObject(self, thread_name):
        #update results_object for thread with results
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()

        #update results widget for selected thread
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

Будут оценены любые комментарии по этому подходу (например, недостатки, подводные камни, похвалы и т. Д.).

Разрешение:

В итоге я использовал класс QThread и связанные с ним сигналы и слоты для связи между потоками. Это в первую очередь потому, что моя программа уже использует Qt / PyQt4 для объектов / виджетов GUI. Это решение также потребовало меньше изменений в моем существующем коде для реализации.

Вот ссылка на соответствующую статью Qt, которая объясняет, как Qt обрабатывает потоки и сигналы, http://www.linuxjournal.com/article/9602. Выдержка ниже:

К счастью, Qt разрешает сигналы и слоты для подключения через потоки - до тех пор, пока потоки запускают свои собственные циклы событий. Это гораздо более чистый метод связь по сравнению с отправкой и получать события, потому что это избегает вся бухгалтерия и промежуточная Производные от QEvent классы, которые становятся необходимо в любом нетривиальном приложение. Общение между темы теперь становится вопросом подключение сигналов от одного потока к слоты в другом и мьютексинг и вопросы безопасности потоков обмена данные между потоками обрабатываются Qt.

Зачем нужно запускать событие цикл внутри каждого потока, к которому вы хотите подключить сигналы? Причина имеет отношение к внутреннему потоку механизм связи, используемый Qt при подключении сигналов от одного нить в слот другой нити. Когда такое соединение сделано, это упоминается как соединение в очереди. Когда сигналы испускаются через соединение в очереди, слот вызывается в следующий раз объект назначения цикл обработки событий выполнен. Если слот вместо этого был вызван непосредственно сигнал из другого потока, этот слот будет выполняться в том же контексте, что и вызывающая нить. Обычно это не то, что вы хотите (и особенно нет что вы хотите, если вы используете подключение к базе данных, как база данных соединение может использоваться только нить которая его создала). В очереди соединение правильно отправляет сигнал объекту потока и вызывает свой слот в своем собственном контексте спекуляция на системе событий. Это именно то, что мы хотим для связь между потоками, в которой некоторые потоки обрабатывают соединения с базой данных. Qt Сигнал / слот механизм находится в корне реализация межпотоковой схема передачи событий, описанная выше, но с гораздо чище и более простой в использовании интерфейс.

ПРИМЕЧАНИЕ: eliben также имеет хороший ответ, и если бы я не использовал PyQt4, который обрабатывает безопасность потоков и мьютексирование, его решение было бы моим выбором.

Ответы [ 5 ]

10 голосов
/ 22 февраля 2009

Если вы хотите использовать сигналы для обозначения прогресса в главном потоке, то вам действительно следует использовать класс QThread PyQt вместо класса Thread из модуля потоков Python.

Простой пример, который использует QThread, сигналы и слоты, можно найти в PyQt Wiki:

https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots

5 голосов
/ 20 июня 2009

Собственные очереди Python не будут работать, потому что вы должны заблокировать в очереди get (), которая забивает ваш пользовательский интерфейс.

Qt по существу реализует систему массового обслуживания изнутри для связи между потоками. Попробуйте этот вызов из любой темы, чтобы опубликовать звонок в слот.

QtCore.QMetaObject.invokeMethod ()

Это неуклюже и плохо документировано, но оно должно делать то, что вы хотите, даже из потока не из Qt.

Для этого вы также можете использовать механизм событий. Смотрите QApplication (или QCoreApplication) для метода с именем что-то вроде "post".

Редактировать: Вот более полный пример ...

Я создал свой собственный класс на основе QWidget. У него есть слот, который принимает строку; Я определяю это так:

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...

Позже я создаю экземпляр этого виджета в основном потоке графического интерфейса. Из основного потока GUI или любого другого потока (стук по дереву) я могу позвонить:

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))

Неуклюжий, но он доставит тебя туда.

Dan.

4 голосов
/ 21 февраля 2009

Я рекомендую вам использовать Очередь вместо сигнализации. Лично я считаю, что это гораздо более надежный и понятный способ программирования, потому что он более синхронный.

Потоки должны получать "задания" из очереди и возвращать результаты в другую очередь. Тем не менее, третья очередь может использоваться потоками для уведомлений и сообщений, таких как ошибки и «отчеты о проделанной работе». Как только вы структурируете свой код таким способом, им станет намного проще управлять.

Таким образом, одна группа «Очередь заданий» и «Очередь результатов» также может использоваться группой рабочих потоков, она направляет всю информацию из потоков в основной поток GUI.

1 голос
/ 21 февраля 2009

Если ваш метод "processDoc" не изменяет никакие другие данные (просто ищет некоторые данные и возвращает их и не изменяет переменные или свойства родительского класса), вы можете использовать макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS ( см. Здесь для подробностей ) в нем. Таким образом, документ будет обработан в потоке, который не будет блокировать интерпретатор, и пользовательский интерфейс будет обновлен.

0 голосов
/ 20 февраля 2009

У вас всегда будет эта проблема в Python. Google GIL "глобальная блокировка интерпретатора" для получения дополнительной информации. Существует два обычно рекомендуемых способа решения проблемы, с которой вы сталкиваетесь: используйте Twisted или используйте модуль, аналогичный multiprocessing , представленному в 2.5.

Twisted потребует, чтобы вы изучили методы асинхронного программирования, которые могут поначалу сбивать с толку, но будут полезны, если вам когда-нибудь понадобится писать высокопроизводительные сетевые приложения, и в долгосрочной перспективе будут более полезными для вас.

Многопроцессорный модуль будет форкировать новый процесс и использует IPC, чтобы заставить его вести себя так, как если бы вы имели истинную многопоточность. Единственным недостатком является то, что вам потребуется установленный Python 2.5, который является довольно новым и по умолчанию включен в большинство дистрибутивов Linux или OSX.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...