7

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

我的 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

追溯

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

5 回答 5

9

如果你想做对的话,使用 PyGTK 线程化有点棘手。基本上,您不应该从除主线程之外的任何其他线程中更新 GUI(GUI 库中的常见限制)。通常这是在 PyGTK 中使用队列消息机制(用于工作人员和 GUI 之间的通信)完成的,这些消息使用超时功能定期读取。一旦我在本地 LUG 上就该主题进行了演示,您就可以从Google 代码存储库中获取此演示的示例代码。看看MainWindowclass in 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 表示)时更新 GUI,不会导致冻结。

  • 1:创建线程
  • 2:创建空闲定时器
  • 3:守护线程,因此应用程序可以在不等待线程完成的情况下关闭
  • 4:启动线程
于 2009-01-28T10:13:37.540 回答
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()生成器对象将运行生成器,直到它到达yieldor return。您只需告诉 GUI 框架long_calculation(some_param, some_callback).next在有时间时调用,最终将使用结果调用您的回调。

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

不幸的是,没有好的一般情况答案。如果您明确说明您要做什么,那么解释为什么在这种情况下不需要线程会更容易。

于 2009-01-27T05:10:24.907 回答
1

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

于 2009-01-27T06:24:06.617 回答
0

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

make_idle 是一个函数装饰器,允许您在后台协同运行任务。这是一个很好的中间地带,介于足够短以在 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 ...

我还没有完成它,但是结合纯粹在空闲和纯粹在线程中做的事情,我有一个装饰器(尚未测试,所以没有发布),你可以告诉它是否要运行 yield 之后的下一部分在 UI 的空闲时间或线程中。这将允许在 UI 线程中进行一些设置,切换到一个新线程来执行后台操作,然后切换到 UI 的空闲时间进行清理,从而最大限度地减少对锁的需求。

于 2009-01-29T14:28:03.107 回答
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()
于 2009-02-02T17:18:43.573 回答