Асинхронное приложение ч.1 / tkinter 15

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Планирование действий

Базовый метод предотвращения блокировки основного потока в Tkinter — это планирование действий, которые будут выполнены после истечения заданного времени.

В этом материале разберемся с тем, как реализовать этот подход в Tkinter с помощью метода after(), который может быть вызван во всех классах виджетов.

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

Это приложение состоит из одной кнопки, которая становится неактивной после нажатия. Через 5 секунд ее снова можно нажать. Простейшая реализация будет выглядеть следующим образом:


import time
import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
time.sleep(5)
self.button.config(state=tk.NORMAL)

if __name__ == "__main__":
app = App()
app.mainloop()

Если запустить эту программу, то можно заметить, что не кнопка становится неактивной, а весь графический интерфейс зависает на 5 секунд. Это понятно по внешнему виду кнопки, которая в течение 5 секунд выглядит нажатой, а не выключенной. Более того, строка заголовка не будет реагировать на клики мыши все это время:

Асинхронное приложение ч.1 / tkinter 15

Если активировать дополнительные виджеты, например, Entry и Scroll, то это поведение задело бы и их.

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

Как работает планирование действий

Метод after() позволяет регистрировать функцию обратного вызова, которая вызывается после задержки, заданной в миллисекундах в основном цикле Tkinter. По сути, они представляют собой зарегистрированные сигналы-события, которые обрабатываются в те моменты, когда система находится в состоянии ожидания.

Таким образом заменим вызов time.sleep(5) на self.after(5000,callback). Используем экземпляр self, потому что метод after() также доступен в корневом экземпляре Tk, и нет разницы в том, чтобы вызывать его из дочернего виджета:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
self.after(5000, lambda: self.button.config(state=tk.NORMAL))

if __name__ == "__main__":
app = App()
app.mainloop()

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

Планирование действий

По последнему примеру можно предположить, что метод after() исполняется после заданной в миллисекундах длительности.

Однако на самом деле метод просит Tkinter зарегистрировать событие, что гарантирует, что оно не выполнится ранее намеченного времени. И если основной поток занят, то верхнего предела того, когда оно все-таки выполнится, нет.

Нужно также помнить о том, что выполнение метода продолжается сразу же после планирования действия. Следующий пример иллюстрирует такое поведение:


print("Первый")
self.after(1000, lambda: print("Третий"))
print("Второй")

Этот код выведет «Первый», «Второй» и, наконец, «Третий» спустя секунду. Все это время графический интерфейс будет оставаться доступным, а пользователи смогут продолжать взаимодействовать с ним.

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

Не стоит забывать, что любая запланированная функция будет выполнена в основном потоке, поэтому одного только after() недостаточно, чтобы предотвратить зависание интерфейса. Нужно также не выполнять методы, выполнение которых занимает много времени в качестве обратного вызова.

В следующем примере рассмотрим, как можно сделать так, чтобы эти блокирующие действия выполнялись в отдельных потоках.

Метод after() возвращает идентификатор запланированного события, который можно передать в метод after_cancel() для отмены выполнения функции обратного вызова.

Дальше рассмотрим, как реализовать остановку запланированного события с помощью этого метода.

Работа в потоках

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

Стандартная библиотека Python включает модуль threading для создания и контроля несколько потоков с помощью высокоуровневого интерфейса, который позволяет работать с простыми классами и методами.

Стоит отметить, что CPython — «эталонная реализация» Python — ограничена GIL (Global Interpreter Lock), механизмом, который не дает нескольким потокам запускать байт-код Python одновременно. Из-за этого невозможно пользоваться преимуществами многопроцессорных систем. Об этом важно помнить при попытке улучшить производительность приложения.

В следующем примере объединены приостановка потока с помощью time.sleep(), а также действие, запланированное с помощью after():


import time
import threading
import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
thread = threading.Thread(target=self.run_action)
print(threading.main_thread().name)
print(thread.name)
thread.start()
self.check_thread(thread)

def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
self.button.config(state=tk.NORMAL)

def run_action(self):
print("Запуск длительного действия...")
time.sleep(5)
print("Длительное действие завершено!")

if __name__ == "__main__":
app = App()
app.mainloop()

Как работают треды

Для создания нового объекта Thread можно использовать конструктор и аргумент-ключевое слово target. Он будет вызван на отдельном потоке при использовании его же метода start().

В прошлом примере использовалась ссылка на метод run_action, примененная экземпляру текущего приложения:


thread = threading.Thread(target=self.run_action)
thread.start()

После этого периодически опрашивается статус потока после after(), который планирует тот же метод до завершения потока:


def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
self.button.config(state=tk.NORMAL)

В прошлом примере задержка была 100 миллисекунд, потому что чаще проверять статус нет необходимости. Хотя это всегда зависит от типа действия на потоке.

Это процесс может быть представлен в виде такой диаграммы:

Асинхронное приложение ч.1 / tkinter 15

Прямоугольник Thread-1 представляет время, во время которого поток занят выполнением time.sleep(5). В то же время MainThread только проверяет статус, и нет ни одной операции, которая приводила бы к зависанию всего интерфейса.

В этом материале мы познакомились с классом Thread, но важно остановиться на некоторых деталях создания их экземпляров и использования в программах на Python.

Методы Thread — start, run и join

start() в этом примере вызывался для выполнения метода в отдельном потоке, чтобы основной продолжал выполняться.

Если же вызвать join(), то основной был бы заблокирован до остановки нового. Это привело бы к тому же «зависанию», которого мы пытались избежать, даже при использовании нескольких потоков.

Наконец, метод run() — это то, где поток выполняет операцию. В будущем его нужно перезаписывать.

Важно запомнить, что нужно всегда вызывать start() из основного потока, чтобы не блокировать его.

Параметры для метода

При использовании конструктора класса Thread можно задать аргументы для передаваемого метода с помощью параметра args:


def start_action(self):
self.button.config(state=tk.DISABLED)
thread = threading.Thread(target=self.run_action, args=(5,))
thread.start()
self.check_thread(thread)

def run_action(self, timeout):
# ...

Параметр self передается автоматически, поскольку используется текущий экземпляр для ссылки на переданный метод. Это удобно в тех ситуациях, когда новому потоку нужен доступ к информации из экземпляра вызвавшего его.

Появились вопросы? Задайте на Яндекс Кью

У блога есть сообщество на Кью, подписывайтесь >> Python Q << и задавайте вопросы. Спрашивайте по контенту, про python и программирование в целом. Обещаю отвечать.

Вам помогла эта статья? Поделитесь в соцсетях или блоге. Репосты помогают сайту развиться.

Тест на знание python

Какая из следующих функций проверяет, что все символы строки в верхнем регистре?
Что выведет этот код?
Что выведет этот код?
Какой будет результат выполнения кода — print(type(1J)) ?
Что выведет этот код?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.