Pull to refresh

Потоки в wxPython

Reading time14 min
Views5.7K
Original author: Mike
При написании программ на Python, используя при этом графический интерфейс иногда приходится запускать различные долгие обработки каких либо данных, при этом в большинстве случаев будет блокироваться интерфейс и пользователь увидит программу замороженной. Чтобы этого избежать необходимо нашу задачу запустить в параллельном потоке или процессе. В данной статье мы рассмотрим, как это сделать в wxPython с помощью модуля Threading.

Потокобезопасные методы wxPython


В wxPython существуют три метода для работы с потоками. Если ими не пользоваться, то при обновлении интерфейса программы Python могут подвиснуть. Чтобы этого избежать, необходимо использовать потокобезопасные методы: wx.PostEvent, wx.CallAfter и wx.CallLater. По словам Robin Dunn (создатель wxPython) wx.CallAfter использует wx.PostEvent для отправки события на объект приложения. Приложение будет иметь обработчик этого события и будет реагировать на него соответственно заложенному алгоритму. На сколько я понимаю wx.CallLater вызывает wx.CallAfter с заданным параметром времени, чтобы он знал сколько ему ждать перед отправкой события.

Robin Dunn также отметил, что Global Interpreter Lock (GIL) не допустит одновременного выполнения более одного потока, что может ограничить количество используемых ядер процессора. С другой стороны, он также сказал, что wxPython освобождается от GIL вызывая API функции библиотеки wx, поэтому другие потоки могут работать одновременно. Другими словами быстродействие может изменяться при использовании потоков на многоядерных машинах. Обсуждение этого вопроса может быть интересным и не понятным…
Прим. перев. — для более полного знакомства с GIL прошу сюда.

Наши три метода можно разделить на уровни абстракции, wx.CallLater находится на самом верху, далее идет wx.CallAfter, а wx.PostEvent находится на самом низком уровне. В следующих примерах вы увидите, как использовать wx.CallAfter и wx.PostEvent в программах WxPython.

wxPython, Threading, wx.CallAfter и PubSub


В списке рассылки wxPython вы увидите, что эксперты говорят другим пользователям использовать wx.CallAfter совместно с PubSub, чтобы обмениваться сообщениями между приложением и потоками. В следующем примере мы это продемонстрируем:
Copy Source | Copy HTML
  1. import time
  2. import wx
  3.  
  4. from threading import Thread
  5. from wx.lib.pubsub import Publisher
  6.  
  7. ########################################################################
  8. class TestThread(Thread):
  9.     """Test Worker Thread Class."""
  10.  
  11.     #----------------------------------------------------------------------
  12.     def __init__(self):
  13.         """Init Worker Thread Class."""
  14.         Thread.__init__(self)
  15.         self.start() # start the thread
  16.  
  17.     #----------------------------------------------------------------------
  18.     def run(self):
  19.         """Run Worker Thread."""
  20.         # This is the code executing in the new thread.
  21.         for i in range(6):
  22.             time.sleep(10)
  23.             wx.CallAfter(self.postTime, i)
  24.         time.sleep(5)
  25.         wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
  26.  
  27.     #----------------------------------------------------------------------
  28.     def postTime(self, amt):
  29.         """<br/>        Send time to GUI<br/>        """
  30.         amtOfTime = (amt + 1) * 10
  31.         Publisher().sendMessage("update", amtOfTime)
  32.  
  33. ########################################################################
  34. class MyForm(wx.Frame):
  35.  
  36.     #----------------------------------------------------------------------
  37.     def __init__(self):
  38.         wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
  39.  
  40.         # Add a panel so it looks the correct on all platforms
  41.         panel = wx.Panel(self, wx.ID_ANY)
  42.         self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
  43.         self.btn = btn = wx.Button(panel, label="Start Thread")
  44.  
  45.         btn.Bind(wx.EVT_BUTTON, self.onButton)
  46.  
  47.         sizer = wx.BoxSizer(wx.VERTICAL)
  48.         sizer.Add(self.displayLbl,  0, wx.ALL|wx.CENTER, 5)
  49.         sizer.Add(btn,  0, wx.ALL|wx.CENTER, 5)
  50.         panel.SetSizer(sizer)
  51.  
  52.         # create a pubsub receiver
  53.         Publisher().subscribe(self.updateDisplay, "update")
  54.  
  55.     #----------------------------------------------------------------------
  56.     def onButton(self, event):
  57.         """<br/>        Runs the thread<br/>        """
  58.         TestThread()
  59.         self.displayLbl.SetLabel("Thread started!")
  60.         btn = event.GetEventObject()
  61.         btn.Disable()
  62.  
  63.     #----------------------------------------------------------------------
  64.     def updateDisplay(self, msg):
  65.         """<br/>        Receives data from thread and updates the display<br/>        """
  66.         t = msg.data
  67.         if isinstance(t, int):
  68.             self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  69.         else:
  70.             self.displayLbl.SetLabel("%s" % t)
  71.             self.btn.Enable()
  72.  
  73. #----------------------------------------------------------------------
  74. # Run the program
  75. if __name__ == "__main__":
  76.     app = wx.PySimpleApp()
  77.     frame = MyForm().Show()
  78.     app.MainLoop()

В этом примере используется модуль time для того, чтобы создать фейковый процесс, выполняющийся долгое время. Однако вы можете использовать что-то более полезное вместо него. В реальном примере я использую поток, чтобы открыть Adobe Reader и послать PDF на печать. Данная операция может показаться незначительной, но когда я не использую потоки, то кнопка печати в моем приложении остается нажатой пока документ отправляется на принтер и интерфейс остается замороженным, пока действие не будет завершено. Даже секунда или две примечательны пользователю!

Так или иначе, давайте посмотрим, как это работает. В нашем классе потока (описанном ниже), мы переопределили метод «run» так, как нам необходимо. Этот поток запускается, когда мы создаем его, потому что у нас есть «self.start()» в __init__ методе. В методе «run» мы в цикле, через каждые 10 секунд, 6 раз вызываем обновление нашего интерфейса, используя wx.CallAfter и PubSub. Когда цикл завершился, мы посылаем финальное сообщение в наше приложение, чтобы сообщить об этом пользователю.

Copy Source | Copy HTML
  1. ########################################################################
  2. class TestThread(Thread):
  3.     """Test Worker Thread Class."""
  4.  
  5.     #----------------------------------------------------------------------
  6.     def __init__(self):
  7.         """Init Worker Thread Class."""
  8.         Thread.__init__(self)
  9.         self.start() # start the thread
  10.  
  11.     #----------------------------------------------------------------------
  12.     def run(self):
  13.         """Run Worker Thread."""
  14.         # This is the code executing in the new thread.
  15.         for i in range(6):
  16.             time.sleep(10)
  17.             wx.CallAfter(self.postTime, i)
  18.         time.sleep(5)
  19.         wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
  20.  
  21.     #----------------------------------------------------------------------
  22.     def postTime(self, amt):
  23.         """<br/>        Send time to GUI<br/>        """
  24.         amtOfTime = (amt + 1) * 10
  25.         Publisher().sendMessage("update", amtOfTime)


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

Последняя интересная часть нашего кода это PubSub, который принимает событие и обработчик самого события:

Copy Source | Copy HTML
  1. def updateDisplay(self, msg):
  2.     """<br/>    Receives data from thread and updates the display<br/>    """
  3.     t = msg.data
  4.     if isinstance(t, int):
  5.         self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  6.     else:
  7.         self.displayLbl.SetLabel("%s" % t)
  8.         self.btn.Enable()


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

Потоки и wx.PostEvent


Ниже приведенный код основывается на данном примере wxPython wiki. Данный код немного сложнее, ранее приведенного, но думаю, разобраться в нем труда особого не составит.

Copy Source | Copy HTML
  1. import time
  2. import wx
  3.  
  4. from threading import Thread
  5.  
  6. # Define notification event for thread completion
  7. EVT_RESULT_ID = wx.NewId()
  8.  
  9. def EVT_RESULT(win, func):
  10.     """Define Result Event."""
  11.     win.Connect(-1, -1, EVT_RESULT_ID, func)
  12.  
  13. class ResultEvent(wx.PyEvent):
  14.     """Simple event to carry arbitrary result data."""
  15.     def __init__(self, data):
  16.         """Init Result Event."""
  17.         wx.PyEvent.__init__(self)
  18.         self.SetEventType(EVT_RESULT_ID)
  19.         self.data = data
  20.  
  21. ########################################################################
  22. class TestThread(Thread):
  23.     """Test Worker Thread Class."""
  24.  
  25.     #----------------------------------------------------------------------
  26.     def __init__(self, wxObject):
  27.         """Init Worker Thread Class."""
  28.         Thread.__init__(self)
  29.         self.wxObject = wxObject
  30.         self.start() # start the thread
  31.  
  32.     #----------------------------------------------------------------------
  33.     def run(self):
  34.         """Run Worker Thread."""
  35.         # This is the code executing in the new thread.
  36.         for i in range(6):
  37.             time.sleep(10)
  38.             amtOfTime = (i + 1) * 10
  39.             wx.PostEvent(self.wxObject, ResultEvent(amtOfTime))
  40.         time.sleep(5)
  41.         wx.PostEvent(self.wxObject, ResultEvent("Thread finished!"))
  42.  
  43. ########################################################################
  44. class MyForm(wx.Frame):
  45.  
  46.     #----------------------------------------------------------------------
  47.     def __init__(self):
  48.         wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
  49.  
  50.         # Add a panel so it looks the correct on all platforms
  51.         panel = wx.Panel(self, wx.ID_ANY)
  52.         self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
  53.         self.btn = btn = wx.Button(panel, label="Start Thread")
  54.  
  55.         btn.Bind(wx.EVT_BUTTON, self.onButton)
  56.  
  57.         sizer = wx.BoxSizer(wx.VERTICAL)
  58.         sizer.Add(self.displayLbl,  0, wx.ALL|wx.CENTER, 5)
  59.         sizer.Add(btn,  0, wx.ALL|wx.CENTER, 5)
  60.         panel.SetSizer(sizer)
  61.  
  62.         # Set up event handler for any worker thread results
  63.         EVT_RESULT(self, self.updateDisplay)
  64.  
  65.     #----------------------------------------------------------------------
  66.     def onButton(self, event):
  67.         """<br/>        Runs the thread<br/>        """
  68.         TestThread(self)
  69.         self.displayLbl.SetLabel("Thread started!")
  70.         btn = event.GetEventObject()
  71.         btn.Disable()
  72.  
  73.     #----------------------------------------------------------------------
  74.     def updateDisplay(self, msg):
  75.         """<br/>        Receives data from thread and updates the display<br/>        """
  76.         t = msg.data
  77.         if isinstance(t, int):
  78.             self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  79.         else:
  80.             self.displayLbl.SetLabel("%s" % t)
  81.             self.btn.Enable()
  82.  
  83. #----------------------------------------------------------------------
  84. # Run the program
  85. if __name__ == "__main__":
  86.     app = wx.PySimpleApp()
  87.     frame = MyForm().Show()
  88.     app.MainLoop()


Давайте разберем его немного. Первые три части для меня являются самыми запутанными:

Copy Source | Copy HTML
  1. # Define notification event for thread completion
  2. EVT_RESULT_ID = wx.NewId()
  3.  
  4. def EVT_RESULT(win, func):
  5.     """Define Result Event."""
  6.     win.Connect(-1, -1, EVT_RESULT_ID, func)
  7.  
  8. class ResultEvent(wx.PyEvent):
  9.     """Simple event to carry arbitrary result data."""
  10.     def __init__(self, data):
  11.         """Init Result Event."""
  12.         wx.PyEvent.__init__(self)
  13.         self.SetEventType(EVT_RESULT_ID)
  14.         self.data = data


EVT_RESULT_ID здесь является ключом, который, по всей видимости, связывает функцию EVT_RESULT с классом ResultEvent. С помощью функции EVT_RESULT мы устанавливаем обработчик событий, которые генерирует наш поток. Функция wx.PostEvent направляет события из потока в класс ResultEvent, которые затем обрабатываются ранее установленным обработчиком событий.

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

Заключение


Надеюсь, вы теперь знаете, как использовать основные методы работы с потоками в своих программах wxPython. Есть несколько других методов для работы с потоками, но в данной статье они не рассматриваются, например wx.Yield или Queues (очереди). К счастью, вики wxPython довольно хорошо охватывают эти темы, так что не забудьте проверить ссылки приведенные ниже, если вы заинтересованы в этих методах.

Дополнительный материал


Tags:
Hubs:
+21
Comments6

Articles

Change theme settings