2009-01-27 127 views
7

作为一个新的Python开发GUI(使用pyGTK)的人,我刚开始学习线程。为了测试我的技能,我编写了一个简单的带有启动/停止按钮的小GTK界面。目标是当它被点击时,一个线程启动,快速增加文本框中的数字,同时保持GUI的响应。初学者的Python线程问题

我已经得到了GUI的工作很好,但我有线程的问题。这可能是一个简单的问题,但我的想法是一天的油炸。下面我首先贴上了Python解释器的引用,然后是代码。你可以去http://drop.io/pxgr5id下载它。我使用bzr进行版本控制,因此如果您想进行修改并重新删除它,请进行修改。我也在http://dpaste.com/113388/上粘贴代码,因为它可以有行号,而这种降价的东西让我很头疼。

更新1月27日,美国东部时间15:52: 稍微更新的代码可以在这里找到:http://drop.io/threagui/asset/thread-gui-rev3-tar-gz

回溯

[email protected]:~/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() 

回答

9

如果你想做正确的话,用PyGTK进行线程化有点棘手。基本上,你不应该在主线程之外的任何其他线程中更新GUI(GUI库中的常见限制)。通常这是在PyGTK中使用排队消息机制(用于工作人员和GUI之间的通信)完成的,这些消息是使用超时功能定期读取的。一旦我在本地LUG上就此主题进行了演示,您可以从Google Code repository获取此演示文稿的示例代码。看看forms/frmmain.py中的MainWindow类,特别是方法_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

一般来说它的更好尽可能避免线程。要正确编写一个线程化的应用程序是非常困难的,甚至更难以知道你是否正确。由于您正在编写一个GUI应用程序,因此您更容易想象如何这样做,因为您必须在异步框架内编写应用程序。

要实现的重要一点是GUI应用程序正在做很多事。它花费大部分时间等待操作系统告诉它发生了一些事情。只要你知道如何编写长时间运行的代码,这样就不会阻塞,你可以在这个空闲时间内做很多事情。

您可以使用超时解决您的原始问题;告诉你的GUI框架在延迟后回调一些函数,然后重置该延迟或开始另一个延迟的调用。

另一个常见问题是如何在GUI应用程序中通过网络进行通信。网络应用程序就像GUI应用程序一样,它们做了很多等待。使用网络IO框架(如Twisted)可以让您的应用程序的两个部分轻松地协作而不是竞争性地等待,并且再次缓解对额外线程的需求。

长时间运行的计算可以迭代地写入而不是同步写入,并且可以在GUI空闲时进行处理。您可以使用一个生成器在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()直到它到达或者yieldreturn将运行发电机。如果有时间,你可以告诉GUI框架调用long_calculation(some_param, some_callback).next,最终你的回调函数将被调用。

我不太了解GTK,所以我不能告诉你应该调用哪些gobject函数。但是,通过这种解释,您应该能够在文档中找到必要的功能,或者在最坏的情况下,在相关的IRC频道上询问。

不幸的是没有好的一般情况下的答案。如果你确切地说明你想要做什么,那么解释为什么你不需要线程就更容易了。

+0

HTTPS而不是使用超时,这样://launchpad.net/rotor-control-software 为什么我不需要RCS类中的线程,特别是从GUI调用Start()时?我在这里发布的这个小小的虚拟程序实际上只是让线程工作的练习。 – crashsystems 2009-01-27 05:40:36

1

您无法重新启动停止的线程对象;不要尝试。相反,如果要在真正停止并加入之后重新启动对象,请创建该对象的新实例。

0

我玩过不同的工具来帮助清理线程,空闲处理等工作。

make_idle是一个函数decorato r允许您在后台合作运行任务。这是在UI线程中运行一次并且不会影响应用程序的响应性并在特殊同步中完全执行线程的短暂事件之间的一个很好的中间基础。在装饰函数内部,您使用“yield”将处理重新传递给GUI,以便它可以保持响应,下一次UI闲置时,它将在您离开的函数中拾取。所以为了让这个开始,你只需要调用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 

如果你需要做更多的处理,你可以使用上下文管理器锁定在UI线程在需要时帮助使代码更加安全

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

用,你可以只

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

我还没有完成它以后还,但在一个线程纯粹闲置和纯粹的结合做的事情,我有一个装饰(未测试却又如此没有发布),您可以告诉它是否下一节将在UI的空闲时间或线程中运行良率。这将允许人们在UI线程中进行一些设置,切换到用于执行后台操作的新线程,然后切换到UI的空闲时间进行清理,从而最大限度地减少对锁的需求。

0

我还没有看到你的代码的细节。但我看到了两个解决方案:

根本不使用线程。镇守像这样

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()