Проблемы с потоками Python начального уровня
Как новичок в разработке 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
Проследить
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 ответов
Потоки с PyGTK немного сложны, если вы хотите сделать это правильно. По сути, вы не должны обновлять GUI из какого-либо другого потока, кроме основного (общее ограничение в библиотеках 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: начать поток
Как правило, лучше избегать тем, когда это возможно. Очень трудно правильно написать многопоточное приложение, и еще труднее узнать, правильно ли вы это сделали. Поскольку вы пишете приложение с графическим интерфейсом, вам проще представить, как это сделать, поскольку вы уже должны писать свое приложение в асинхронной среде.
Важно понимать, что приложение с графическим интерфейсом ничего не делает. Он проводит большую часть своего времени, ожидая, пока ОС скажет ему, что что-то произошло. Вы можете делать много вещей в этот простой, если вы знаете, как писать долго работающий код, чтобы он не блокировался.
Вы можете решить исходную проблему, используя тайм-аут; указание вашей графической структуре вызвать некоторую функцию после задержки, а затем сбросить эту задержку или начать другой отложенный вызов.
Другой распространенный вопрос - как общаться по сети в приложении с графическим интерфейсом. Сетевые приложения похожи на приложения с графическим интерфейсом в том смысле, что они много ждут. Использование сетевой инфраструктуры ввода-вывода (например, 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.
К сожалению, нет хорошего общего ответа. Если вы объясните, что именно вы пытаетесь сделать, было бы проще объяснить, почему вам не нужны потоки в этой ситуации.
Вы не можете перезапустить остановленный объект потока; не пытайся Вместо этого создайте новый экземпляр объекта, если вы хотите перезапустить его после того, как он действительно будет остановлен и присоединен.
Я играл с различными инструментами, чтобы помочь очистить работу с потоками, обработкой простоя и т. Д.
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. в простое пользовательского интерфейса или в потоке. Это позволило бы выполнить некоторые настройки в потоке пользовательского интерфейса, переключиться на новый поток для выполнения фоновых операций, а затем переключиться на простой пользовательского интерфейса для выполнения очистки, сводя к минимуму необходимость блокировок.
Я не посмотрел подробно на ваш код. Но я вижу два решения вашей проблемы:
Не используйте темы вообще. Вместо этого используйте тайм-аут, как это:
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()