Страницы

Поиск по вопросам

пятница, 5 апреля 2019 г.

Мультизадачность на Python: выполнить две долгие функции одновременно, не блокируя GUI

Всем доброго времени суток. Изучаю возможности графического модуля tkinter на Python. Возникла необходимость одновременного запуска нескольких функций, т.е. имеется, например, две кнопки, каждая со своим функционалом. Нужно сделать так, чтобы окно с кнопками после нажатия на одну из них не зависало и позволяло нажать другую кнопку, незамедлительно запустив ее функцию. Пишу на питоне версии 3.5.2.
from tkinter import *
def whilefunc1(): n = 1 while (n <= 100000): print(n) n = n + 1
def whilefunc2(): n = 100001 while (n <= 200000): print(n) n = n + 1
root = Tk() root.geometry("200x200")
but1 = Button(root, text="press me", command = whilefunc1) but1.pack() but2 = Button(root, text="me 2!!", command = whilefunc2) but2.pack()
root.mainloop()


Ответ

Общая особенность GUI программирования в том, что не следует использовать блокирующие функции в обработчиках событий, которые выполняются как правило в GUI потоке, иначе это приводит к "подвисанию" GUI.
В вашем случае вместо self.status[ip] = os.system(f'ping {ip}') можно использовать self.process[ip] = subprocess.Popen(['ping', ip]) и позже проверять статус используя self.process[ip].poll() (оба вызова возвращаются сразу, не дожидаясь пока команда завершится). Периодический опрос можно организовать используя root.after(), например как в start_process(). Можно избежать опроса, если настроить обработчик сигнала, который вызывается когда запущенный дочерний процесс завершается, например, на Unix (усложнённая опция в данном случае). Cross-platform пример кода это QProcess.finished сигнал из Qt GUI библиотеки, который позволяет подключить свой обработчик, который вызывается, когда команда, запущенная QProcess::start, завершается (finished сигнал реализован эффективно, используя функциональность, предоставляемую соответствующей операционной системой, не требуя периодического опроса статуса запущенного дочернего процесса).
Альтернативно, если необходимо запустить числодробительную задачу (CPU-bound), то для этого можно создать пул процессов:
from concurrent.futures import ProcessPoolExecutor
pool = ProcessPoolExecutor()
и использовать command=lambda: pool.submit(whilefunc1) в качестве обработчика событий для кнопки (.submit() ставит выполнение whilefunc1() функции в очередь и сразу же возвращается). Если вы нажмёте 1000 раз на кнопку, то вы скорее всего не хотите запускать 1000 процессов (системе вероятно плохо будет). ProcessPoolExecutor() по умолчанию использует все доступные CPU ядра, не запуская излишние процессы. В этом смысле, это решение более предпочтительно чем решения, использующие threading.Thread(..).start() или multiprocessing.Process(..).start(), где каждое нажатие на кнопку приводит к запуску нового потока/процесса.
Если задача связана с вводом-выводом, то можно использовать Widget.tk.createfilehandler() или аналогичную функциональность в других GUI библиотеках (например, GObject.io_add_watch()). Вот пример кода, где root.createfilehandler() используется чтобы читать вывод внешней команды, не дожидаясь её завершения.
Поддержка createfilehandler() может быть ограничена на некоторых платформах (Windows), поэтому ту же задачу можно переносимо выполнить используя фоновый поток (попытка использовать socketpair, чтобы получить переносимый код, оказалась неуспешной: Popen() не принимает соответствующий fileno() на Windows).

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

Отправить комментарий