Tkinter. Асинхронное приложение (часть 1)

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

Современные ПК сообщают о том, что пользователю придется подождать. Способы разные – прогрессбары, изменение цвета или значка курсора. Это указывает на то, что программа выполняет процесс, требующий больше времени, чем обычно, и пользователю надо подождать. То есть, программа не зависла.

Но, в целом, такое поведение приложения – не очень хорошо. Чтобы решить эту проблему, можно поручить этот процесс свободному ядру процессора. Собственно, для этого и нужна асинхронность в программировании. Современная программа должна уметь это делать.

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

Чтобы основной поток 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 секунд после того, как вы нажмете кнопку. Внешний вид кнопки после этого также изменится. Она будет выглядеть, как нажатая, а не выключенная.

Кроме того, даже строка заголовка не будет реагировать на клики мыши в течение этого времени.Tkinter. Асинхронное приложение (часть 1)

Если бы в программе были другие элементы, например, Entry и Scroll, то и в этом случае программа бы зависла. 

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

Принципы планирования действий в Python

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

В приведенном выше фрагменте кода заменим time.sleep(5) на self.after(5000, callback). Почему мы использовали экземпляр self? Все потому, что этот метод доступен в корневом экземпляре 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()

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

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

Tkinter. Асинхронное приложение (часть 1)

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

И если основной поток сейчас совершает какие-то операции, то тогда вообще нет верхнего предела времени на выполнение.

Важно также помнить о том, что метод продолжает выполняться, как только действие было спланировано. Давайте приведем фрагмент кода для наглядности.  

print("Первый")

self.after(1000, lambda: print("Третий"))

print("Второй")

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

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

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

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

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

Теперь рассмотрим, как останавливать запланированное событие, используя этот метод.

Выполнение работы в потоках

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

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

Единственный момент – ограничение GIL. Это механизм, который мешает использовать байт-код 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()

Принцип работы тредов

Чтобы создавать новый тред, применяется конструктор и аргумент-ключевое слово 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 миллисекунд, поскольку более часто проверять статус не нужно. Правда, здесь все зависит от типа действия на потоке.

Методы 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 осуществляется автоматически, так как применяется текущий элемент, чтобы ссылаться на переданный метод. Это подходит для тех ситуаций, когда новый поток требует доступ к данным из экземпляра, который его вызвал. 

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

ОфисГуру
Adblock
detector