Проблемы с потоками Python начального уровня - PullRequest
7 голосов
/ 27 января 2009

Как новичок в разработке GUI на Python (с pyGTK), я только начал изучать многопоточность. Чтобы проверить свои навыки, я написал простой маленький интерфейс GTK с кнопкой «старт / стоп». Цель состоит в том, чтобы при щелчке запускался поток, который быстро увеличивал число в текстовом поле, сохраняя при этом графический интерфейс пользователя.

У меня графический интерфейс работает нормально, но у меня проблемы с потоками. Вероятно, это простая проблема, но я думаю о том, чтобы пожарить на день. Ниже я вставил сначала трекбек из интерпретатора Python, а затем код. Вы можете перейти на http://drop.io/pxgr5id, чтобы загрузить его. Я использую bzr для контроля версий, поэтому, если вы хотите внести изменения и повторно отбросить их, передайте изменения. Я также вставляю код в http://dpaste.com/113388/, потому что у него могут быть номера строк, и этот материал уценки вызывает у меня головную боль.

Обновление 27 января, 15:52 EST: Слегка обновленный код можно найти здесь: http://drop.io/threagui/asset/thread-gui-rev3-tar-gz

Traceback

crashsystems@crashsystems-laptop:~/Desktop/thread-gui$ python threadgui.pybtnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 39, in on_btnStartStop_clicked
    self.thread.stop()
  File "threadgui.py", line 20, in stop
    self.join()
  File "/usr/lib/python2.5/threading.py", line 583, in join
    raise RuntimeError("cannot join thread before it is started")
RuntimeError: cannot join thread before it is started
btnStartStop clicked
threadStop = 1
btnStartStop clicked
threadStop = 0
btnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 36, in on_btnStartStop_clicked
    self.thread.start()
  File "/usr/lib/python2.5/threading.py", line 434, in start
    raise RuntimeError("thread already started")
RuntimeError: thread already started
btnExit clicked
exit() called

код

#!/usr/bin/bash
import gtk, threading

class ThreadLooper (threading.Thread):
    def __init__ (self, sleep_interval, function, args=[], kwargs={}):
        threading.Thread.__init__(self)
        self.sleep_interval = sleep_interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.finished = threading.Event()

    def stop (self):
        self.finished.set()
        self.join()

    def run (self):
        while not self.finished.isSet():
            self.finished.wait(self.sleep_interval)
            self.function(*self.args, **self.kwargs)

class ThreadGUI:
    # Define signals
    def on_btnStartStop_clicked(self, widget, data=None):
        print "btnStartStop clicked"
        if(self.threadStop == 0):
            self.threadStop = 1
            self.thread.start()
        else:
            self.threadStop = 0
            self.thread.stop()
        print "threadStop = " + str(self.threadStop)

    def on_btnMessageBox_clicked(self, widget, data=None):
        print "btnMessageBox clicked"
        self.lblMessage.set_text("This is a message!")
        self.msgBox.show()

    def on_btnExit_clicked(self, widget, data=None):
        print "btnExit clicked"
        self.exit()

    def on_btnOk_clicked(self, widget, data=None):
        print "btnOk clicked"
        self.msgBox.hide()

    def on_mainWindow_destroy(self, widget, data=None):
        print "mainWindow destroyed!"
        self.exit()

    def exit(self):
        print "exit() called"
        self.threadStop = 1
        gtk.main_quit()

    def threadLoop(self):
        # This will run in a thread
        self.txtThreadView.set_text(str(self.threadCount))
        print "hello world"
        self.threadCount += 1

    def __init__(self):
        # Connect to the xml GUI file
        builder = gtk.Builder()
        builder.add_from_file("threadgui.xml")

        # Connect to GUI widgets
        self.mainWindow = builder.get_object("mainWindow")

        self.txtThreadView = builder.get_object("txtThreadView")
        self.btnStartStop = builder.get_object("btnStartStop")
        self.msgBox = builder.get_object("msgBox")
        self.btnMessageBox = builder.get_object("btnMessageBox")
        self.btnExit = builder.get_object("btnExit")
        self.lblMessage  = builder.get_object("lblMessage")
        self.btnOk = builder.get_object("btnOk")

        # Connect the signals
        builder.connect_signals(self)

        # This global will be used for signaling the thread to stop.
        self.threadStop = 1

        # The thread
        self.thread = ThreadLooper(0.1, self.threadLoop, (1,0,-1))
        self.threadCounter = 0

if __name__ == "__main__":
    # Start GUI instance
    GUI = ThreadGUI()
    GUI.mainWindow.show()
    gtk.main()

Ответы [ 5 ]

9 голосов
/ 28 января 2009

Потоки с PyGTK немного сложны, если вы хотите сделать это правильно. По сути, вы не должны обновлять графический интерфейс из какого-либо другого потока, кроме основного (общее ограничение в библиотеках GUI). Обычно это делается в PyGTK с использованием механизма сообщений в очереди (для связи между рабочими и GUI), которые периодически читаются с использованием функции тайм-аута. После того, как у меня была презентация на локальном LUG по этой теме, вы можете получить пример кода для этой презентации из Google Code репозитория . Взгляните на класс MainWindow в forms/frmmain.py, специально для метода _pulse() и на то, что делается в on_entry_activate() (поток запускается там и создается таймер простоя).

def on_entry_activate(self, entry):
    text = entry.get_text().strip()
    if text:
        store = entry.get_completion().get_model()
        if text not in [row[0] for row in store]:
            store.append((text, ))
        thread = threads.RecommendationsFetcher(text, self.queue)# <- 1
        self.idle_timer = gobject.idle_add(self._pulse)# <- 2
        tv_results = self.widgets.get_widget('tv_results')
        model = tv_results.get_model()
        model.clear()
        thread.setDaemon(True)# <- 3
        progress_update = self.widgets.get_widget('progress_update')
        progress_update.show()
        thread.start()# <- 4

Таким образом, приложение обновляет графический интерфейс, когда оно «бездействует» (означает GTK), не вызывая зависаний.

  • 1: создать тему
  • 2: создать таймер простоя
  • 3: демонизировать поток, чтобы приложение можно было закрыть без ожидания завершения потока
  • 4: начальная тема
3 голосов
/ 27 января 2009

Как правило, лучше избегать потоков, когда это возможно. Очень сложно правильно написать потоковое приложение, и еще труднее узнать, правильно ли вы это сделали. Поскольку вы пишете приложение с графическим интерфейсом, вам проще представить, как это сделать, поскольку вы уже должны писать свое приложение в асинхронной среде.

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

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

Другой распространенный вопрос - как общаться по сети в приложении с графическим интерфейсом. Сетевые приложения похожи на приложения с графическим интерфейсом в том смысле, что они много ждут. Использование сетевой инфраструктуры ввода-вывода (например, Twisted ) упрощает совместное ожидание обеих частей приложения, а не конкуренции, и снова устраняет необходимость в дополнительных потоках.

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

def long_calculation(param, callback):
    result = None
    while True:
        result = calculate_next_part(param, result)
        if calculation_is_done(result):
            break
        else:
            yield
    callback(result)

Вызов long_calculation даст вам объект генератора, а вызов .next() для объекта генератора будет запускать генератор, пока он не достигнет либо yield, либо return. Вы просто скажете инфраструктуре графического интерфейса для вызова long_calculation(some_param, some_callback).next, когда у нее будет время, и в итоге ваш обратный вызов будет вызван с результатом.

Я не очень хорошо знаю GTK, поэтому не могу сказать, какие функции gobject вы должны вызывать. Однако с этим объяснением вы сможете найти необходимые функции в документации или, в худшем случае, обратиться к соответствующему каналу IRC.

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

1 голос
/ 27 января 2009

Вы не можете перезапустить остановленный объект потока; не пытайся Вместо этого создайте новый экземпляр объекта, если вы хотите перезапустить его после того, как он действительно остановился и присоединился.

0 голосов
/ 02 февраля 2009

Я не посмотрел подробно в вашем коде. Но я вижу два решения вашей проблемы:

Не используйте темы вообще. Вместо этого используйте тайм-аут, например:

import gobject

i = 0
def do_print():
    global i
    print i
    i += 1
    if i == 10:
        main_loop.quit()
        return False
    return True

main_loop = gobject.MainLoop()
gobject.timeout_add(250, do_print)
main_loop.run()

При использовании потоков вы должны убедиться, что код GUI вызывается только из одного потока одновременно, защищая его следующим образом:

import threading
import time

import gobject
import gtk

gtk.gdk.threads_init()

def run_thread():
    for i in xrange(10):
        time.sleep(0.25)
        gtk.gdk.threads_enter()
        # update the view here
        gtk.gdk.threads_leave()
    gtk.gdk.threads_enter()
    main_loop.quit()
    gtk.gdk.threads_leave()

t = threading.Thread(target=run_thread)
t.start()
main_loop = gobject.MainLoop()
main_loop.run()
0 голосов
/ 29 января 2009

Я играл с различными инструментами, чтобы помочь очистить работу с потоками, обработкой простоя и т. Д.

make_idle - это декоратор функций, который позволяет вам совместно запускать задачи в фоновом режиме. Это хорошее промежуточное звено между чем-то достаточно коротким, чтобы запускаться один раз в потоке пользовательского интерфейса и не влиять на скорость отклика приложения, и выполнением полного потока в специальной синхронизации. Внутри декорированной функции вы используете «yield», чтобы передать обработку обратно графическому интерфейсу, чтобы он мог оставаться отзывчивым, и в следующий раз, когда пользовательский интерфейс простаивает, он включится в вашу функцию, где вы остановились. Итак, чтобы начать это, просто вызовите idle_add для оформленной функции.

def make_idler(func):
    """
    Decorator that makes a generator-function into a function that will
continue execution on next call
    """
    a = []

    @functools.wraps(func)
    def decorated_func(*args, **kwds):
        if not a:
            a.append(func(*args, **kwds))
        try:
            a[0].next()
            return True
        except StopIteration:
            del a[:]
            return False

    return decorated_func

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

@contextlib.contextmanager
def gtk_critical_section():
    gtk.gdk.threads_enter()
    try:
        yield
    finally:
        gtk.gdk.threads_leave()

с этим вы можете просто

with gtk_critical_section():
    ... processing ...

Я еще не закончил с этим, но, комбинируя выполнение задач исключительно в режиме ожидания и исключительно в потоке, у меня есть декоратор (еще не проверенный, но не опубликованный), с помощью которого можно определить, будет ли следующий раздел после yield быть запущенным во время простоя пользовательского интерфейса или в потоке. Это позволило бы выполнить некоторые настройки в потоке пользовательского интерфейса, переключиться на новый поток для выполнения фоновых операций, а затем переключиться на простой пользовательского интерфейса для выполнения очистки, сводя к минимуму необходимость блокировок.

...