Как отслеживать ход выполнения потока в Python, не замораживая графический интерфейс PyQt?

StackOverflow https://stackoverflow.com/questions/569650

Вопрос

Вопросы:

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

Предыстория вопроса:

  • У меня есть графический интерфейс PyQt для Windows.
  • Он используется для обработки наборов документов HTML.
  • Для обработки набора документов требуется от трех секунд до трех часов.
  • Я хочу иметь возможность обрабатывать несколько подходов одновременно.
  • Я не хочу, чтобы графический интерфейс блокировался.
  • Я смотрю на модуль потока, чтобы достичь этого.
  • Я относительно новичок в резьбе.
  • Графический интерфейс имеет один индикатор выполнения.
  • Я хочу, чтобы он показал прогресс выбранного потока.
  • Отобразить результаты выбранного потока, если он закончен.
  • Я использую 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 для объектов/виджетов графического интерфейса.Для реализации этого решения также потребовалось меньше изменений в существующем коде.

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

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

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

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

QtCore.QMetaObject.invokeMethod()

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

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

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

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

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

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

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

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

Дэн.

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

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

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

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

В Python у вас всегда будет эта проблема.Google GIL «глобальная блокировка интерпретатора» для получения дополнительной информации.Обычно рекомендуется использовать два способа обойти возникшую проблему:использовать витой, или используйте модуль, аналогичный многопроцессорность модуль, представленный в версии 2.5.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top