Почему рабочий поток ухудшает отзывчивость графического интерфейса?

#python #pyqt5 #python-multithreading #qthread #qrunnable

Вопрос:

Я пытаюсь понять, почему этот Worker поток, который намеренно использует довольно интенсивную обработку (в частности, сортировку этих словарей), приводит к тому, что поток графического интерфейса перестает отвечать. Вот MRE:

 from PyQt5 import QtCore, QtWidgets
import sys, time, datetime, random

def time_print(msg):
    ms_now = datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')
    thread = QtCore.QThread.currentThread()
    print(f'{thread}, {ms_now}: {msg}')
    
def dict_reorder(dictionary):
    return {k: v for k, v in sorted(dictionary.items())}
    
class Sequence(object):
    n_sequence = 0
    simple_sequence_map = {}
    sequence_to_sequence_map = {}
    prev_seq = None
    
    def __init__(self):
        Sequence.n_sequence  = 1 
        if Sequence.n_sequence % 1000 == 0:
            print(f'created sequence {Sequence.n_sequence}')
        rand_int = random.randrange(100000)
        self.text = str(rand_int)
        Sequence.simple_sequence_map[self] = rand_int
        if Worker.stop_ordered:
            time_print(f'init() A: stop ordered... stopping now')
            return
        dict_reorder(Sequence.simple_sequence_map)
        if Sequence.prev_seq:
            Sequence.sequence_to_sequence_map[self] = Sequence.prev_seq
            if Worker.stop_ordered:
                time_print(f'init() B: stop ordered... stopping now')
                return
            dict_reorder(Sequence.sequence_to_sequence_map)
        Sequence.prev_seq = self

    def __lt__(self, other):
        return self.text < other.text    
        
class WorkerSignals(QtCore.QObject):
    progress = QtCore.pyqtSignal(int)
    stop_me = QtCore.pyqtSignal()

class Worker(QtCore.QRunnable):
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.signals = WorkerSignals()
        
    def stop_me_slot(self):
        time_print('stop me slot')    
        Worker.stop_ordered = True
    
    @QtCore.pyqtSlot()
    def run(self):
        total_n = 30000
        Worker.stop_ordered = False
        for n in range(total_n):
            progress_pc = int(100 * float(n 1)/total_n)
            self.signals.progress.emit(progress_pc)
            Sequence()
            if Worker.stop_ordered:
                time_print(f'run(): stop ordered... stopping now, n {n}')
                return

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        layout = QtWidgets.QVBoxLayout()
        self.progress = QtWidgets.QProgressBar()
        layout.addWidget(self.progress)
        
        start_button = QtWidgets.QPushButton('Start')
        start_button.pressed.connect(self.execute)
        layout.addWidget(start_button)
        
        self.stop_button = QtWidgets.QPushButton('Stop')
        layout.addWidget(self.stop_button)
        
        w = QtWidgets.QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()
        self.threadpool = QtCore.QThreadPool()
        self.resize(800, 600)

    def execute(self):
        self.worker = Worker()
        self.worker.signals.progress.connect(self.update_progress)
        self.stop_button.pressed.connect(self.worker.stop_me_slot)
        self.threadpool.start(self.worker)
        
    def update_progress(self, progress):
        self.progress.setValue(progress)        
              
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
app.exec_()
 

На моей машине примерно до 12% графический интерфейс значительно не реагирует: кнопки не приобретают цвет «наведения» (светло-синий) и, похоже, не могут быть нажаты (хотя нажатие «Стоп» вызывает остановку через много секунд). Периодически появляется страшный спиннер (синий кружок в ОС W10).

Примерно через 12% становится возможным нормально использовать кнопки.

Что я делаю не так?

Ответ №1:

Очень простое решение состоит в том, чтобы «переспать» поток с помощью базового time.sleep : даже с очень небольшим интервалом это даст достаточно времени основному потоку для обработки своей очереди событий, избегая блокировки пользовательского интерфейса:

     def run(self):
        total_n = 30000
        Worker.stop_ordered = False
        for n in range(total_n):
            progress_pc = int(100 * float(n 1)/total_n)
            self.signals.progress.emit(progress_pc)
            Sequence()
            if Worker.stop_ordered:
                time_print(f'run(): stop ordered... stopping now, n {n}')
                return
            time.sleep(.0001)
 

Примечание: этот pyqtSlot декоратор бесполезен, потому что он работает только для подклассов QObject (которыми QRunnable не является); вы можете удалить его.

Ответ №2:

Python не может запускать более одного потока с интенсивным использованием процессора. Причина этого-ГИЛ. В основном потоки Python не годятся ни для чего, кроме ожидания ввода-вывода.

Если вам нужна часть с интенсивным использованием процессора, попробуйте либо переписать интенсивную часть с помощью Cython, либо использовать multiprocessing , но тогда время для отправки данных туда и обратно может быть значительным.

Комментарии:

1. Спасибо! Это действительно полезная подсказка. Я просто немного удивлен, что поток графического интерфейса классифицируется как «поток с интенсивным использованием процессора». Возможно, вы имеете в виду, точнее, что если есть ОДИН поток с интенсивным использованием процессора, он на практике не может сосуществовать с ЛЮБЫМИ другими потоками в том же процессе?

2. Я склоняюсь к тому, чтобы переписать в Rust… который уже пару лет кажется новым сексуальным ребенком в квартале. Он специально разработан с учетом многопоточности, и, по-видимому, доступны привязки Qt.

3. @mikerodent: Поток графического интерфейса, к сожалению, может быть чувствителен к процессору. то есть, чтобы в нужное время требовалось достаточно процессора, чтобы не чувствовать себя неспокойно. musicamante выше предложил решение именно для этого: управление потоком с интенсивным использованием процессора, чтобы поток графического интерфейса имел возможность работать чаще.

Ответ №3:

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

ГИЛ может быть проблемой, как предполагает @9000.

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

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

 dict_reorder(Sequence.simple_sequence_map)
 

с

 Sequence.simple_sequence_map = dict_reorder(Sequence.simple_sequence_map)
 

и

 dict_reorder(Sequence.sequence_to_sequence_map)
 

с

 Sequence.sequence_to_sequence_map = dict_reorder(Sequence.sequence_to_sequence_map)
 

Комментарии:

1. Спасибо, очень интересно. Я отключил сигналы о прогрессе if n % 100 == 0: , и проблема, похоже, решена! На самом деле я не искал способ оптимизировать эту сортировку словаря: на самом деле я потратил немного времени на поиск чего-то (похожего на мой реальный проект), что намеренно очень интенсивно. Вы говорите, что даже при всех сигналах о прогрессе у вас нет проблем. Не могли бы вы сказать, на какой операционной системе вы находитесь? Ваша машина отличается особенно высокими техническими характеристиками? Это i7-7700 3,6 ГГц с большой оперативной памятью и т. Д.

2. Если уж на то пошло, что вы подразумеваете под «запуском без отладчика»? Я только что погуглил это, и мне не совсем ясно… Я просто бегу python my_package в CLI. Есть ли стандартный отладчик для Python, который вам нужно активно отключить? (простите мое невежество, если это так…)

3. @mikerodent Я думал, что вы запускаете его в среде ide с подключенным отладчиком, но вы запускаете его в терминале, поэтому отладчик не задействован. Я запускаю его на win7 на не современном ноутбуке (Core i7 4510U) в режиме настроек энергосбережения наряду с интенсивным процессом видеокодирования процессора (и без него) и не зависал, я запускаю его в win10 (Core i5 4590), и иногда у меня было всего несколько секундных задержек.