Дата и время публикации :
Назначение и использование
1. Назначение
Реализуемая многозадачность в PyGtk, или просто Gtk, предназначена для вынесения операций за скобки главного цикла событий (Main Event Loop(, чтобы исключить его блокировку, а вместе с ним и всего пользовательского интерфейса. Такой подход позволяет одновременно выполнять в фоне или, как модно нынче произносить «юзать в бэкграунде», следующие операции:
- оценки производительности и воздействия внешних факторов;
- чтение / запись данных в синхронном и асинхронном режиме доступа, подразумевающие сервисных операций для обмена данными с сервером WEB, база данных и т.п.
Из всех выбранных примеров, рассмотрим применение многозадачности, на примере организации записи текстовых сообщений в графический элемент редактор текста с интервалом 1 секунду, результат реализации которого показан на рисунке 1.1
Рисунок 1.1
Также, в процессе записи текста в графический элемент Gtk, мы научимся отделять яйца от курицы, а моряков от салаг... Потому что, в используемой многозадачности Gtk, нужно уметь разделять объекты программы, в соответствие со следующими факторами, на:
- используемые только внутри переключаемых потоков и не попадающих в графический интерфейс Gtk, вернее Main Event Loop;
- элементы графического интерфейса Gtk, которые отрисовываются в Main Event Loop родителя, который и породил потоки.
Если не учитывать эти два фактора разделения объектов программы, то можно бесконечно удивляться к различным сбоям в её работе и постоянным падениям из-за игнорирования главного цикла событий Gtk.
2. Использование
2.1 Обращение по таймеру
Самое просто и доступное, чем пестрит сеть "Интернет", это использование функции обратного вызова по таймеру, как показано в листинге 2.1.1 кусок примера, который взял из сети "Интернет".
Листинг 2.1.1
... sub_proc = Popen("ping -c 10 localhost", stdout=PIPE, shell=True) ... def update_terminal(): textviev.set_text(non_block_read(sub_proc.stdout)) return sub_proc.poll() is None ... gobject.timeout_add(100, update_terminal) gtk.main() ...
Конечно, приведенный пример "заводить" по таймеру получение данных из некоего устройства вывода просто, но несет угрозы в виде возможности блокировки главного цикла событий, например, в случае "подвисания" процедуры чтения или падения самой программы.
2.2 Использования многозадачности
Предоставляется импортируемым модулем threading, который предоставляет класс запуска и управления многопоточностью Thread, как показано в листинге 2.2.1
Листинг 2.2.1
... 56 import threading 57 import time ... 64 class GThread(threading.Thread): 65 66 # constructor for a threading.Thread that will run a function in thread 67 def __init__(self, *args, **keywords) : 68 threading.Thread.__init__(self, *args, **keywords) 69 self.killed = False 70 self.start(self) 71 72 def kill(self): 73 self.killed = True 74 75 def isKill(self): 76 return self.killed 77 78 def sleep(self,sec) : 79 time.sleep(sec) ...
В листинге 2.2.1 показан конструктор класс GThread, который с использованием базового класса Thread реализует создание и запуск потока. Порядок использования класса GThread показан в листинге 2.2.2
Листинг 2.2.2
... 152 class TitleBarTopWindow (Gtk.HeaderBar): ... 186 def run(self): 187 while not self.thread.isKill() : 189 self.termwin.text_insert_in_newline('thread running...' ) 190 self.thread.sleep(1) 191 self.termwin.text_insert_in_newline('thread break...' ) 192 193 def on_run_clicked(self, button): 194 # make to icon name change 195 if self.start_name == "call-start" : ... 197 self.thread=GTerminal.GThread(target=self.run) 198 self.thread.start() ... 199 else: ... 202 self.thread.kill() ...
Как показано в строках 186-191 листинга 2.2.2 до создания экземпляра объекта класса GTerminal.GThread() ему передается метод TitleBarTopWindow.Run(), который будет затем передан аргументу target в конструкторе Gthread
Соответственно, создание объекта self.thread осуществляется в строке 197 данного листинга. При этом, изменение поведения потока self.thread индицируется свойством класса GThread.killed, объявляемый в строке 69 листинга 2.2.1 и который указывает завершился поток или нет. При этом, как показано в строке 73 того же листинга, свое свойство GThread.killed он меняет после вызова метода GThread.kill() в строке 202 листинга 2.2.2
2.3 Межзадачная блокировка
Она требуется для корректного завершения потока и повторного запуска, о которых особенно никто не пишет. При этом, блокировка , реализуемая для threading.Thread, не действуют в случае использования для объектов на базе классов Gtk, потому что необходимо синхронизироваться с методом Gtk.Application.run(), запускающего Main Event Loop приложения. А вот доступ к объектам, которые никогда не будут использованы с этим циклом, нужно синхронизировать с использованием межзадачной блокировки, чтобы исключить гонки за совместно используемые ими вычислительные ресурсы — коими являются память и процессор в первую очередь, а так же периферия с "дровами" ядра ОС — во вторую.
Блокировка для управления доступа к свойствам класса Gthread, не затрагивающие объекты, наследующие базовые классы Gtk и отдельно их объекты, реализуется через методы threading.Rlock.aquired() / threading.Rlock.release(), как показано в листинге 2.3.1 ( выделено жирным).
Листинг 2.3.1
... 68 class GThread(threading.Thread): 69 70 # constructor for a threading.Thread that will run a function in thread 71 def __init__(self, *args, **keywords) : 72 threading.Thread.__init__(self, *args, **keywords) 73 self.rlock = threading.RLock() 74 self.killed = not self.isKill() 75 self.reused = False 76 77 # start the thread 78 def try_to_start(self): 79 delays = 0 80 self.start() 81 while self.isKill() and delays < 10 : 82 delays += 1 83 self.sleep(0.01) 84 85 if delays < 10 : 86 isrlock=self.rlock.acquire() 87 if isrlock == True : 88 self.killed = False 89 self.rlock.release() 90 self.reused = True 99 100 # kills the thread 101 def kill(self): 102 103 isrlock = self.rlock.acquire() 104 if isrlock == True : 105 self.killed = True 106 self.rlock.release() 107 108 # return a flag that thread has been killed 109 def isKill(self): 110 111 isrlock=self.rlock.acquire() 112 if isrlock == True : 113 iskilled = False 114 if not self.is_alive() or self.killed : 115 iskilled = True 116 self.rlock.release() 117 return iskilled ...
Первое , как можно увидеть из листинга 2.3.1, что код усложнился и появился декларируемый в строках 78,…,90 метод Gthread.try_to_start(), а Gthread.kill() и Gthread.isKill() – внушительно разбухли, как показано в строках 100-106 и 109,..., 117 соответственно. Давайте разберемся почему это произошло?
Потому что свойство Gthread.killed является объектом с конкурирующим доступом между потоком и породившем его родителем, как было показано ранее в строке 196 листинга 2.2.2, к тому же Gthread.RLock является реентабельной блокировкой, которая может применяться неоднократно к одному и тому же потоку в одном и том же порядке. Поэтому, соответственно, применяется практически одна и таже конструкция, как показано в строке 86 листинга 2.3.2
Листинг 2.3.2
... 86 isrlock=self.rlock.acquire() 87 if isrlock == True : 88 self.killed = False 89 self.rlock.release() ...
Где в строке 86 листинга 2.3.2, пытаемся взять блокировку, а в строке 87 проверяем, что именно данный поток вызвал её. Затем, в строке 88 изменяем значение свойства с конкурирующим доступом Gthread.killed и затем, отпускаем блокировку в строке 89 все того же листинга.
2.4 Повторное использование
Для повторного использования, как показано в строке 75 листинга 2.3.1, введено свойство Gthread.reused, которое становится истинным после первого успешного запуска потока в методе Gthread.try_to_start(), как показано было ранее в строке 90 того же листинга.
Это свойство используется родителем, порождающего поток, а именно в TitleBarTopWindow.on_run_clicked(), как показано в листинге 2.4.1
Листинг 2.4.1
... 152 class TitleBarTopWindow (Gtk.HeaderBar): ... 188 def on_run_clicked(self, button): ... 193 if self.thread.reused : 194 self.thread=GTerminal.GThread(target=self.run) 195 self.thread.try_to_start() ...
В котором показано, что если не создать повторно экземпляр класса объекта GThread с передачей ему замещающую функцию GThread.run(), как показано в строках 193 и 194 дампа 2.4.1, можно наткнуться на аварийное завершение программы с сообщением об ошибке, текст которой показан в дампе 2.4.2
Дамп 2.4.2
... RuntimeError: threads can only be started once ...
Оно,сообщение об ошибке, возникает при повторном выполнении метода try_to_start() в строке 195 листинга 2.4.1
2.5 Повторное использование
И все бы было прекрасно, если бы не одно НО?! А именно возникающая ошибка, которая показана в дампе 2.5.1
Дамп 2.5.1
... Gtk:ERROR:../../../../gtk/gtktextlayout.c:2455:gtk_text_layout_get_line_display: code should not be reached ...
Она, эта ошибка, указывает на "несанкционированный выход за границы (контекста) потока" (an illegal cross-thread boundary violation) и возникает при обращении к объекту self.termwin, порожденный от производного класса Gtk.ViewText, не смотря на выполненную только что межзадачную синхронизацию в методе thread.isKill()
В Gtk ошибка ранее успешно лечилась с использованием объявдение и освобождение критической секцией (выделено жирным), как показано в листинге 2.5.2
Листинг 2.5.2
... 152 class TitleBarTopWindow (Gtk.HeaderBar): ... 204 def run(self): 205 number = 0 206 while not self.thread.isKill() : 207 Gdk.threads_enter() 208 self.termwin.text_insert_in_newline('thread running, stage #' + str(number) ) 209 Gdk.threads_leave() 210 self.thread.sleep(1) 211 number += 1 ... 243 class TopLevelWindow(Gtk.ApplicationWindow): 244 245 # constructor for a Gtk.TopLevelWindow 246 def __init__(self, app): 247 Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL, modal=True, title="top", application=app) ... 264 topbar= TitleBarTopWindow(self,"Automatic run the shell programs",self.termon) 265 self.set_titlebar(topbar) ... 274 class GShellAutorunApp(Gtk.Application): 275 276 def __init__(self): 278 Gtk.Application.__init__(self) 279 280 def do_activate(self): 281 top = TopLevelWindow(self) 282 top.show_all() 283 if __name__ == '__main__': 284 Gdk.threads_init() ...
В строке 207 и 209 листинга 2.5.2 показано, что с помощью Gdk.threads_enter() для участка кода, имеющий конкурирующий доступом к Main Event Loop программы, выделяется критической секции вместе обращения к объекту конкурирующего доступа Gtk, а затем с Gdk.threads_leave() – она освобождается. При этом, во время активации приложения необходимо было вызвать Gdk.threads_init(), как показано в строке 284 того же листинга.
И как говорится, что всего бы ничего, но Gdk.thread_enter()/Gdk.threads_leave() может не сработать. А все потому, что код этих методов из него скоро будет выкинут на всегда. Даже не смотря на постоянные просьбы "телезрителей", а диагностические сообщения из предупреждающих превратятся в утверждающие. Пример вывода которых показан в дампе 2.5.3
Дамп 2.5.3
... .../gshell-autorun:284: DeprecationWarning: Gdk.threads_init is deprecated Gdk.threads_init() .../gshell-autorun:207: DeprecationWarning: Gdk.threads_enter is deprecated Gdk.threads_enter() .../gshell-autorun:209: DeprecationWarning: Gdk.threads_leave is deprecated Gdk.threads_leave()
Из только что приведенного дампа видно, что на сегодняшний день Gdk.threads_init(), Gdk.threads_enter()/Gdk.threads_leave() являются запрещенными. При этом, об этом свидетельствует документация на Gtk, начиная с версии 3.6, вышедшей уже "сто лет" назад, а именно в далеком 2013 году.
Метод Gdk.threads_add_idle(), а так же и GLib.timeout_add(), можно сравнить с кочегарами подбрасывающие уголек в топку паровоза – главного цикла выполнения программы Gtk.
Пример использования Gdk.threads_add_idle() приводится в листинге 2.5.4
Листинг 2.5.4
... 212 def run(self) : 213 number = 0 214 while not self.thread.isKill() : 215 Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, lambda m: self.termwin.text_insert_in_newline('thread running, stage:' + m), str(number) ) 216 self.thread.sleep(1) 217 number += 1 ...
Тоже самое можно достичь с GLib.timeout_add() , как показано в листинге 2.5.5
Листинг 2.5.5
... 212 def run(self) : 213 number = 0 214 while not self.thread.isKill() : 215 GLib.timeout_add (1, lambda m: self.termwin.text_insert_in_newline('thread running, stage:' + m), str(number) ) 216 self.thread.sleep(1) 217 number += 1 ...
3. Библиография
3.1 Gnome Developer. Multiline text editor
3.2 Python's os and subprocess Popen Commands
3.3 StackOverflow. Show terminal output in a gui window using python Gtk
3.4 StackOverflow. separate threads in pygtk application
3.5 PyGObject.Threads & Concurrency
3.6 threading — Thread-based parallelism
3.7 Python | Different ways to kill a Thread
3.8 StackOverflow. What are the three arguments to gdk threads add idle
3.9 StackOverflow. Python gtk-3 safe threading
3.10 xspdf.com. Gdk_threads_add_idle example
3.11 [Feature] GDK Threads (for asynchronous updates) #550
3.12 Gnome Developer. Method gdk-threads-add-idle"
3.13 GtkD. Idle listeners on the mainloop
3.14 Program Talk - Source Code Browser
3.15 pyGTK and Threading