2025, Nov 12 06:02

Мгновенные обновления GUI в PySide6: вместо Pool.map — imap_unordered

Почему Pool.map блокирует поток GUI в PySide6/Qt и как исправить: используйте imap_unordered или map_async, QThread и очередь для мгновенных обновлений QLabel

Мгновенное отображение прогресса в Qt‑интерфейсе легко пропадает, как только в дело вступает мультипроцессинг. Если UI перестаёт обновляться, пока пул рабочих занят, почти наверняка блокируется главный поток. Ниже — краткое объяснение, почему так случается, и что сделать, чтобы QLabel показывал прогресс сразу по мере появления.

Как воспроизвести проблему

Конфигурация ниже поднимает пул процессов и отправляет строки статуса в общую очередь. QThread передаёт данные из этой очереди в GUI через сигнал Qt. Однако, несмотря на эмит сигналов, метка обновляется только после завершения работы пула.

Модуль с рабочими процессами:

import os
import multiprocessing
notif_queue = multiprocessing.Queue()
class PoolWorker:
    def run_unit(self, idx):
        pid = os.getpid()
        msg = f\"Process id = {pid}\\nReceived nb = {idx}\"
        notif_queue.put(msg)
        print(msg)
    def run_pool(self):
        size = 8
        pool = multiprocessing.Pool(size)
        pool.map(self.run_unit, range(size))

Модуль GUI:

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout
from PySide6.QtCore import QThread, Signal
from engine_mod import PoolWorker, notif_queue
class QueuePump(QThread):
    pushed = Signal(str)
    def run(self):
        while True:
            text = notif_queue.get(block=True)
            self.pushed.emit(text)
class MainPane(QWidget):
    def __init__(self):
        super().__init__()
        self.status_label = QLabel(\"label to update\")
        self.run_button = QPushButton(\"Do Stuff\")
        layout = QVBoxLayout()
        layout.addWidget(self.status_label)
        layout.addWidget(self.run_button)
        self.setLayout(layout)
        self.reader = QueuePump()
        self.reader.pushed.connect(self.on_text)
        self.reader.start()
        self.run_button.clicked.connect(self.on_click)
    def on_text(self, text):
        self.status_label.setText(text)
        print(text)
        self.status_label.update()
        QApplication.processEvents()
    def on_click(self):
        worker = PoolWorker()
        worker.run_pool()
if __name__ == "__main__":
    app = QApplication([])
    ui = MainPane()
    ui.show()
    sys.exit(app.exec())

В чём проблема

Суть проблемы — в использовании Pool.map. Как сказано в документации Python:

«Он блокирует поток до готовности результата.»

Вызов пула из обработчика кнопки загоняет главный поток внутрь map до завершения всех задач. Пока ваш QThread продолжает испускать сигналы, цикл событий Qt не может передать их в слот, потому что поток GUI заблокирован. В итоге все накопленные обновления приходят разом лишь после возврата из пула.

Решение

Переключитесь на неблокирующий API пула, чтобы поток GUI сразу возвращался к циклу событий. imap_unordered как раз так и работает: задачи стартуют, а интерфейс остаётся отзывчивым.

def run_pool(self):
    size = 8
    pool = multiprocessing.Pool(size)
    pool.imap_unordered(self.run_unit, range(size))

С этой заменой метка обновляется сразу, как только процесс кладёт сообщение в очередь и QThread его отправляет. Если требуется выполнить логику после завершения последней задачи, используйте map_async — он тоже не блокирует и поддерживает колбэк по завершению.

Почему это важно

В приложениях, где показывается прогресс множества загрузок, задержки в обновлении интерфейса сводят на нет пользу таких индикаторов. Свободный главный поток позволяет сигналам и слотам Qt срабатывать мгновенно. Важно также понимать характер сетевых задач: обработка запроса/ответа нагружает CPU, тогда как передача данных и дисковое I/O — это операции ввода‑вывода. Крупные пачки передач не всегда выигрывают от большего числа процессов: накладные расходы со временем перевешивают выгоду. Во многих случаях QNetworkAccessManager в Qt обеспечивает асинхронный, основанный на сигналах способ параллельных загрузок без какого‑либо мультипроцессинга и может сочетаться с потоками, когда задействован локальный доступ к данным.

Практические заметки

Когда интерфейс отзывчив, обычно не нужно принудительно перерисовывать виджеты. QLabel.setText сам планирует обновление; явные вызовы вроде QWidget.update и QApplication.processEvents для этого случая не требуются.

Итоги

Если интерфейс Qt перестаёт обновляться при мультипроцессинге, проверьте, нет ли блокирующих вызовов в потоке GUI. Замените Pool.map на неблокирующий вариант, например imap_unordered, чтобы цикл событий продолжал работать, и полагайтесь на QThread как на мост для сообщений о прогрессе. Если нужен хук по завершении всех работ, используйте map_async с колбэком завершения. Для сетевых сценариев оцените, не упростит ли асинхронный API вроде QNetworkAccessManager показ прогресса и уменьшит сложность мультипроцессинга.

Статья основана на вопросе на StackOverflow от Jan Donal и ответе Jan Donal.