Обработка и вывод данных в PyQt5 без зависаний и статуса "программа не отвечает" Python
Я использовала qasync, чтобы при работе программы интерфейс PyQt5 не зависал и чтобы можно было отображать данные в процессе обработки. Всё работало хорошо, но в цикле при долгой обработке Windows ставит программе "не отвечает". По данному вопросу я попробовала сделать обработку в отдельном потоке, это сработало, но возникает два вопроса:
- Я использую асинхронные функции, возможно ли асинхронно запустить поток для дальнейшего запуска функций в нём? Или это неправильно и есть другое решение.
- Если нужно запускать множество функций в отдельном потоке, то для каждой нужно отдельно прописывать или можно описать общий шаблон, через который запускать все функции?
Возможно есть способ всё сделать с помощью асинхронности, без использования отдельный потоков. Тестируя разные варианты, я поняла, что при использовании прогресс бара данные обновляются, но в интерфейсе ничего нельзя нажать или выбрать. Также при использовании await asyncio.sleep(0.001) данные обновляются и при этом параллельно можно нажать/выбрать другой элемент, но я думаю это неверное решение. Я знаю, что в C# в формах есть Application.DoEvents(), есть ли что-то подобное в python при использовании PyQt?
Вот минимальный пример, который при нажатии "Load" загружает строки с файла test.txt и записывает их в массив с использованием await asyncio.sleep(0.001), а при нажатии 'Start' считывает с файла check.txt строки для проверки, затем сравнивает данные и выводит разницу между массивами с применением progress bar.
import asyncio
import functools
from PyQt5.QtGui import QTextCursor
from qasync import asyncSlot, QApplication
import qasync
from PyQt5.QtWidgets import *
import sys
len_value = 0
all_values = []
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.button_load = QPushButton('Load', self)
self.label_load = QLabel('Загружено', self)
self.text_edit = QTextEdit('', self)
self.button_start = QPushButton('Start', self)
self.progress = QProgressBar(self)
self.label_status = QLabel('Выполнено', self)
self.text_result = QTextEdit('', self)
layout = QVBoxLayout(self)
layout.addWidget(self.button_load)
layout.addWidget(self.label_load)
layout.addWidget(self.text_edit)
layout.addWidget(self.button_start)
layout.addWidget(self.label_status)
layout.addWidget(self.progress)
layout.addWidget(self.text_result)
self.button_load.clicked.connect(self.load_text)
self.button_start.clicked.connect(self.check_text)
@asyncSlot()
async def load_text(self):
global all_values, len_value
count = 0
f = open('test.txt', 'r', encoding='utf-8')
value = f.read().split('\n')
len_value = len(value)
for v in value:
count += 1
all_values.append(v)
self.text_edit.insertPlainText(f'{v}\n')
self.text_edit.moveCursor(QTextCursor.End)
self.label_load.setText(f'Загружено {str(count)}/{str(len_value)}')
await asyncio.sleep(0.001)
@asyncSlot()
async def check_text(self):
global all_values
count = 0
value_progress = 100 / len_value
cur_progress_value = 0
f = open('check.txt', 'r', encoding='utf-8')
check_values = f.read().split('\n')
for val in all_values:
cur_progress_value += value_progress
self.progress.setValue(cur_progress_value)
if val not in check_values:
count += 1
self.text_result.insertPlainText(f'{val}\n')
self.text_result.moveCursor(QTextCursor.End)
self.label_status.setText(f'Выполнено {str(count)}')
self.progress.setValue(100)
async def main():
def close_future(future, loop):
loop.call_later(10, future.cancel)
future.cancel("Close Application")
loop = asyncio.get_event_loop()
future = asyncio.Future()
app = QApplication.instance()
if hasattr(app, 'aboutToQuit'):
getattr(app, 'aboutToQuit').connect(functools.partial(close_future, future, loop))
win = Window()
win.show()
await future
return True
if __name__ == '__main__':
try:
qasync.run(main())
except asyncio.exceptions.CancelledError:
sys.exit(0)
Ответы (1 шт):
Начну с того, что await asyncio.sleep(0.001) является полным аналогом Application.DoEvents(), то есть, это способ отдать управление циклу обработки событий, в блокирующей функции. Этот способ, как будет видно ниже, используется в официальном примере из qasync.
Теперь собственно сам пример с моими комментариями, который демонстрирует два возможных подхода, а именно:
- обработка с помощью асинхронных функций
- запуск синхронной/блокирующей функции в выделенном потоке
async def master():
progress = QProgressBar()
progress.setRange(0, 99)
progress.show()
# запуск асинхронной функции
await first_50(progress)
# запуск синхронной функции в отдельном потоке
loop = asyncio.get_running_loop()
with QThreadExecutor(1) as exec:
await loop.run_in_executor(exec, functools.partial(last_50, progress), loop)
async def first_50(progress):
for i in range(50):
progress.setValue(i)
await asyncio.sleep(0.1) # тут отдаем управление, чтоб цикл
# мог обработать другие события
# эта функция выполняется в отдельном потоке
def last_50(progress, loop):
for i in range(50, 100):
# это заставит в основном цикле обработки событий
# выполнить progress.setValue(i)
loop.call_soon_threadsafe(progress.setValue, i)
time.sleep(0.1)
qasync.run(master())
Что касается инфраструктуры для упрощения запуска блокирующих задач в отдельных потоках, то можно сделать класс типа такого:
class BackgroundTaskBase:
def __init__(self):
self.loop = asyncio.get_running_loop()
async def start(self, *args, **kwargs):
with QThreadExecutor(1) as exec:
await self.loop.run_in_executor(
exec, lambda: self.run(*args, **kwargs))
def exec_in_ui(self, method, *args, **kwargs):
self.loop.call_soon_threadsafe(method, *args, **kwargs)
и потом его использовать:
class UpdateProgressTask(BackgroundTaskBase):
def run(self, progress):
for i in range(50, 100):
self.exec_in_ui(progress.setValue, i)
time.sleep(0.1)
async def master():
progress = QProgressBar()
progress.setRange(0, 99)
progress.show()
await UpdateProgressTask().start(progress)