Страницы

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

воскресенье, 2 февраля 2020 г.

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

#python #многопоточность #gui #tkinter


Всем доброго времени суток.
Изучаю возможности графического модуля 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()

    


Ответы

Ответ 1



Общая особенность 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).

Ответ 2



Можно сделать декоратор def thread(fn): def execute(*args, **kwargs): threading.Thread(target=fn, args=args, kwargs=kwargs).start() return execute тогда можно вынести(@thread) выполнение функции в отдельный поток, без изменения остального кода @thread def whilefunc1(): # эта функция всегда будет вызватся в отдельном потоке n = 1 while (n <= 100000): print(n) n = n + 1 but1 = Button(root, text="press me", command = whilefunc1) если выполнение в потоке всегда, ненужно, то задекорировать можно и так def whilefunc2(): n = 100001 while (n <= 200000): print(n) n = n + 1 but2 = Button(root, text="me 2!!", command = thread(whilefunc2)) это избавляет от постоянного объявления threading.Thread

Ответ 3



Ну в общем-то нашел решение: Изменил but1 = Button(root, text="press me", command = whilefunc1) but1.pack() but2 = Button(root, text="me 2!!", command = whilefunc2) but2.pack() на but1 = Button(root, text="press me", command = lambda: threading.Thread(target = whilefunc1).start()) but1.pack() but2 = Button(root, text="me 2!!", command = lambda: threading.Thread(target = whilefunc2).start()) but2.pack() Однако я до сих пор не понимаю зачем там нужна lambda:, однако без нее не работает

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

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