× К оглавлению На главную Об авторе

Дата и время публикации   :

Назначение и использование


1. Назначение

Реализуемая многозадачность в PyGtk, или просто Gtk, предназначена для вынесения операций за скобки главного цикла событий (Main Event Loop(, чтобы исключить его блокировку, а вместе с ним и всего пользовательского интерфейса. Такой подход позволяет одновременно выполнять в фоне или, как модно нынче произносить «юзать в бэкграунде», следующие операции:

Из всех выбранных примеров, рассмотрим применение многозадачности, на примере организации записи текстовых сообщений в графический элемент редактор текста с интервалом 1 секунду, результат реализации которого показан на рисунке 1.1

Рисунок 1.1

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

Если не учитывать эти два фактора разделения объектов программы, то можно бесконечно удивляться к различным сбоям в её работе и постоянным падениям из-за игнорирования главного цикла событий 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