среда, 18 июля 2012 г.

wxPython и нити (Threads) (Перевод)


Если Вы часто используете GUI в Python, тогда Вы знаете, что иногда требуется запускать процессы, выполняющиеся значительное время. И, если Вы реализуете это так же, как делаете в программах для командной строки, Вы будете очень удивлены. В большинстве случаев, Вы увидите просто "зависшую" программу, так как цикл обработки событий будет ждать завершения вашей длительной процедуры. Что в таком случае можно сделать? Запустить эту задачу в отдельной нити или в отдельном процессе! Этим мы с Вами сейчас и займёмся!

wxPython’s Threadsafe методы

В мире wxPython есть три связанных “threadsafe” метода. Если Вы забудете об одном из этих трёх методов, то, при обновлении вашего GUI, Вы можете столкнуться со странными проблемами. Иногда всё будет работать замечательно. Иногда падать без какой-либо причины. Вот эти три метода: wx.PostEvent, wx.CallAfter и wx.CallLater. Согласно Robin Dunn (создатель wxPython), wx.CallAfter использует wx.PostEvent для отправки события объекту приложения. Приложение вызывает связанный с этим событием обработчик и реагирует на него соответствующим образом. Как я понимаю, wx.CallLater вызывает wx.CallAfter через определённый промежуток времени, так что Вы можете определить, сколько Вы хотите подождать перед отправкой события.
Robin Dunn так же указал на то, что Python Global Interpreter Lock (GIL) не позволяет выполняться одновременно более чем одному потоку байт-кода, что не даёт возможности использовать несколько ядер процессора. С другой стороны, он так же сказал, что “wxPython освобождает GIL на время вызова API wx, так что другие потоки могут исполняться в это время”. Другими словами, многоядерность может Вам и помочь. Достаточно интересно и запутанно...
В любом случае, касательно наших трёх методов это значит что wx.CallLater наиболее абстрактный threadsafe метод, за которым следует wx.CallAfter и самый низкоуровневый - это wx.PostEvent. На примерах Вы увидите как можно использовать wx.CallAfter и wx.PostEvent для обновления информации в ваших программах на wxPython.

wxPython, Threading, wx.CallAfter и PubSub

В рассылке wxPython Вы можете встретить экспертов, говорящих об использовании wx.CallAfter вместе с PubSub для обеспечения взаимодействия в их wxPython приложениях в разных потоках. Да и я сам так говорил. Вот пример этого:
import time
import wx
 
from threading import Thread
from wx.lib.pubsub import Publisher
 
########################################################################
class TestThread(Thread):
    """Test Worker Thread Class."""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self.start()    # запустить новый поток
 
    #----------------------------------------------------------------------
    def run(self):
        """Run Worker Thread."""
        # этот код выполняется в новом потоке
        for i in range(6):
            time.sleep(10)
            wx.CallAfter(self.postTime, i)
        time.sleep(5)
        wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
 
    #----------------------------------------------------------------------
    def postTime(self, amt):
        """
        Посылаем время в GUI
        """
        amtOfTime = (amt + 1) * 10
        Publisher().sendMessage("update", amtOfTime)
 
########################################################################
class MyForm(wx.Frame):
 
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
 
        # добавляем панель, чтобы это правильно выглядело на разных платформах
        panel = wx.Panel(self, wx.ID_ANY)
        self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
        self.btn = btn = wx.Button(panel, label="Start Thread")
 
        btn.Bind(wx.EVT_BUTTON, self.onButton)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.displayLbl, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5)
        panel.SetSizer(sizer)
 
        # создаём получателя pubsub
        Publisher().subscribe(self.updateDisplay, "update")
 
    #----------------------------------------------------------------------
    def onButton(self, event):
        """
        Запустить поток
        """
        TestThread()
        self.displayLbl.SetLabel("Thread started!")
        btn = event.GetEventObject()
        btn.Disable()
 
    #----------------------------------------------------------------------
    def updateDisplay(self, msg):
        """
        Получаем данные от потока и обновляем приложение
        """
        t = msg.data
        if isinstance(t, int):
            self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
        else:
            self.displayLbl.SetLabel("%s" % t)
            self.btn.Enable()
 
#----------------------------------------------------------------------
# Запускаем программу
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()
Мы используем модуль time для имитации нашего трудоёмкого процесса. По желанию можете его спокойно чем-нибудь заменить. Я, например, могу открыть в потоке Adobe Reader и послать PDF на печать. Это может казаться странным, но без выделения этого процесса в отдельный поток, моё приложение зависло бы до завершения печати. А у пользователя на счету каждая секунда!
В любом случае давайте посмотрим как это работает. В нашем классе потока (приведён ещё раз ниже) мы переопределяем метод “run” для решения тех задач, которые нам нужны. Этот поток начинает выполняться когда мы создаём экземпляр класса, так как мы поместили вызов “self.start()” в его метод __init__ . В методе “run” мы запускаем цикл от 0 до 6, засыпаем на 10 секунд на каждой итерации и затем обновляем данные в приложении используя wx.CallAfter и PubSub. Когда наш цикл завершается мы посылаем сообщение приложению, чтобы сообщить пользователю об этом.
########################################################################
class TestThread(Thread):
    """Test Worker Thread Class."""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self.start()    # запускаем поток
 
    #----------------------------------------------------------------------
    def run(self):
        """Run Worker Thread."""
        # Этот код выполняется в новом потоке
        for i in range(6):
            time.sleep(10)
            wx.CallAfter(self.postTime, i)
        time.sleep(5)
        wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
 
    #----------------------------------------------------------------------
    def postTime(self, amt):
        """
        Посылаем время в GUI
        """
        amtOfTime = (amt + 1) * 10
        Publisher().sendMessage("update", amtOfTime)
Вы можете заметить, что в нашем коде мы начинаем поток используя обработчик нажатия кнопки.  Кроме того, мы деактивиуем кнопку чтобы не запустить лишний поток, так как два потока одновременно вполне могут нас смутить. Иначе Вы можете отображать PID потока и выводить информацию в многострочное поле чтобы отслеживать работу нескольких потоков.
Последний кусок кода, который может представлять для нас интерес - обработчик получателя PubSub:
def updateDisplay(self, msg):
    """
    Получаем данные от потока и отображаем их
    """
    t = msg.data
    if isinstance(t, int):
        self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
    else:
        self.displayLbl.SetLabel("%s" % t)
        self.btn.Enable()
Видите, как мы получаем данные и отображаем их? Кроме того, мы определяем тип данных, которые получили, для того, чтобы понять, как лучше их отобразить. Круто, правда? Теперь давайте спустимся на уровень ниже и посмотрим на wx.PostEvent.

wx.PostEvent и потоки

Этот код основан на примере из wxPython wiki. Он немного сложнее, чем код с wx.CallAfter, который мы видели, но я уверен, что и он не составит для нас труда.
import time
import wx
 
from threading import Thread
 
# Определяем событие, означающее завершение потока
EVT_RESULT_ID = wx.NewId()
 
def EVT_RESULT(win, func):
    """Определяем событие завершения."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)
 
class ResultEvent(wx.PyEvent):
    """Простое события для разных данных."""
    def __init__(self, data):
        """Инициируем событие завершения."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data
 
########################################################################
class TestThread(Thread):
    """Test Worker Thread Class."""
 
    #----------------------------------------------------------------------
    def __init__(self, wxObject):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self.wxObject = wxObject
        self.start()    # запускаем поток
 
    #----------------------------------------------------------------------
    def run(self):
        """Run Worker Thread."""
        # Код, выполняемый в потоке
        for i in range(6):
            time.sleep(10)
            amtOfTime = (i + 1) * 10
            wx.PostEvent(self.wxObject, ResultEvent(amtOfTime))
        time.sleep(5)
        wx.PostEvent(self.wxObject, ResultEvent("Thread finished!"))
 
########################################################################
class MyForm(wx.Frame):
 
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
 
        # панель для кросс-платформенности
        panel = wx.Panel(self, wx.ID_ANY)
        self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
        self.btn = btn = wx.Button(panel, label="Start Thread")
 
        btn.Bind(wx.EVT_BUTTON, self.onButton)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.displayLbl, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5)
        panel.SetSizer(sizer)
 
        # устанавливаем обработчик для завершения потока
        EVT_RESULT(self, self.updateDisplay)
 
    #----------------------------------------------------------------------
    def onButton(self, event):
        """
        Запускаем поток
        """
        TestThread(self)
        self.displayLbl.SetLabel("Thread started!")
        btn = event.GetEventObject()
        btn.Disable()
 
    #----------------------------------------------------------------------
    def updateDisplay(self, msg):
        """
        Получаем и отображаем данные
        """
        t = msg.data
        if isinstance(t, int):
            self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
        else:
            self.displayLbl.SetLabel("%s" % t)
            self.btn.Enable()
 
#----------------------------------------------------------------------
# запускаем программу
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()
Давайте теперь разбираться. Для меня самыми тяжёлыми были первые три куска:

# Определяем событие, означающее завершение потока
EVT_RESULT_ID = wx.NewId()
 
def EVT_RESULT(win, func):
    """Определяем событие завершения."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)
 
class ResultEvent(wx.PyEvent):
    """Простое события для разных данных."""
    def __init__(self, data):
        """Инициируем событие завершения."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data
EVT_RESULT_ID - это ключ к нашему пониманию. Он связывает поток с wx.PyEvent и с этой странной функцией “EVT_RESULT”. В коде wxPython мы связываем обработчик события с функцией EVT_RESULT. Это позволяет нам использовать wx.PostEvent в потоке для отправки события в наш класс события ResultEvent. Что он делает?  Он отправляет данные в программу wxPython, так как с ним связана наша функция EVT_RESULT. Надеюсь, это понятно.
Теперь перечитайте ещё раз код. Стало яснее? Вот и славно! Можно заметить, что наш класс TestThread почти не изменился в этом варианте от исходного. API для отображения информации тоже осталось старым.

Итог

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

Домашнее чтение

Загрузки

4 комментария:

  1. Очень полезная статья, но примеры для wx.__version__
    '3.0.xxxx не подходят.
    Требуется заменить импорт Publisher

    #from wx.lib.pubsub import Publisher
    from wx.lib.pubsub import setuparg1
    from wx.lib.pubsub import pub

    Ну, и ниже вызовы Publisher(). заменить на pub.

    ОтветитьУдалить
  2. Есть еще замечание для версии 3
    wxPyDeprecationWarning: Using deprecated class PySimpleApp. Use :class:`App` instead.
    app = wx.PySimpleApp()

    Вот так совсем хорошо

    # Запускаем программу
    if __name__ == "__main__":
    app = wx.App()
    frame = MyForm().Show()
    app.MainLoop()

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Использование wx.CallAfter и PubSub для wx.__version__
    '3.0.xxxx в приведенном варианте выскакивает сообщение

    *** Внимание ***
    Этот протокол обмена сообщениями является устаревшим. Этот модуль, и, следовательно, аргумент1
    протокол обмена сообщениями, будут удалены в V3.4 из PyPubSub.

    Вообщем, как бы работает, но типа, мы вас предупреждаем.

    А вот второй вариант с использованием wx.PostEvent работает чисто.

    ОтветитьУдалить