防止pygtk GUI在长时间运行过程中卡死

5 投票
2 回答
3839 浏览
提问于 2025-04-17 08:45

我有一个处理过程,可能需要一两分钟才能完成。当我从我的pygtk图形界面调用这个过程时,窗口在大约10秒后就会锁住(变暗,用户无法操作)。

我想阻止这种情况发生,但我不太确定该怎么做。我以为多线程可以解决这个问题,但似乎并没有奏效。我尝试了网上找到的两种不同方法。首先,我修改了这个常见问题解答,让它可以处理一个运行时间较长的函数。其次,我直接使用了threading.Thread,就像在这个回答中那样,但这也导致了锁定。

下面是我的两个示例。我对多线程还很陌生,所以可能这并不是我想要的解决方案。我基本上只是想让图形界面不锁住,这样我就可以更新进度条,并让用户使用取消按钮。

#Sample 1
import threading
import time
import gobject
import gtk

gobject.threads_init()

class MyThread(threading.Thread):
    def __init__(self, label, button):
        super(MyThread, self).__init__()
        self.label = label
        self.button = button
        self.counter = 0
        button.connect("clicked", self.on_button_click)
        self.quit = False

    def update_label(self, counter):
        self.label.set_text("Counter: %i" % counter)
        time.sleep(20)
        return False

    def on_button_click(self, widget):
        self.counter += 1
        gobject.idle_add(self.update_label, self.counter)

window = gtk.Window()
label = gtk.Label()
box = gtk.VBox()
button = gtk.Button("Test")
box.pack_start(label)
box.pack_start(button)
window.add(box)
window.show_all()
window.connect("destroy", lambda _: gtk.main_quit())
thread = MyThread(label, button)
thread.start()

gtk.main()
thread.quit = True

#####################################
#Sample 2

from threading import Thread
import time
import gobject
import gtk

class Test():
    def __init__(self):
        self.counter = 0
        self.label = gtk.Label()
        button = gtk.Button("Test")

        window = gtk.Window()
        box = gtk.VBox()
        box.pack_start(self.label)
        box.pack_start(button)
        window.add(box)

        window.connect("destroy", lambda _: gtk.main_quit())
        button.connect("clicked", self.on_button_click)
        window.show_all()

    def update_label(self, counter):
        self.label.set_text("Counter: %i" % counter)
        time.sleep(20)
        return False

    def on_button_click(self, widget):
        self.counter += 1
        thread = Thread(target=self.update_label, args=(self.counter,))
        thread.start()
        while thread.is_alive():
            pass
        thread.stop()

test = Test()
gtk.main()

2 个回答

0

你应该为每个线程重新实现一下 Thread.run 方法,并在里面启动一个事件循环。

另外,你可以让按钮的点击事件调用一个线程的 start 方法,这样就会自动调用 run 方法,去执行你的长时间任务。这样的话,你就不需要在每个线程里都搞一个事件循环了。

下面是一些简单的代码,来解释我说的第二种方法:

class MyThread(threading.Thread):

    def __init__(self, label, button):
        threading.Thread.__init__(self)
        self.label = label
        self.button = button
        self.counter = 0

    def run(self):
        time.sleep(20)

def callback():
    label.set_text("Counter: %i" % thread.counter)
    thread.start()

window = gtk.Window()
label = gtk.Label()
box = gtk.VBox()
button = gtk.Button('Test')
box.pack_start(label)
box.pack_start(button)
window.add(box)
window.show_all()

thread = MyThread(label, button)
button.connect('clicked', callback)

我使用了一个回调函数,因为我怀疑 set_text 这个方法在多线程中是不安全的。

7

下面是我修改过的第二个例子,它对我有效:

import threading
import time
import gtk, gobject, glib

gobject.threads_init()

class Test():
    def __init__(self):
        self.counter = 0
        self.label = gtk.Label()
        self.progress_bar = gtk.ProgressBar()
        self.progress_bar_lock = threading.Lock()
        button = gtk.Button("Test")

        window = gtk.Window()

        box = gtk.VBox()
        box.pack_start(self.label)
        box.pack_start(self.progress_bar)
        box.pack_start(button)
        window.add(box)

        window.connect("destroy", lambda _: gtk.main_quit())
        button.connect("clicked", self.on_button_click)
        window.show_all()

    def update_label(self, counter):
        self.label.set_text("Thread started (counter: {0})"
                            .format(counter))
        time.sleep(5)
        self.label.set_text("Thread finished (counter: {0})"
                            .format(counter))
        return False

    def pulse_progress_bar(self):
        print threading.active_count()
        if threading.active_count() > 1:
            self.progress_bar.pulse()
            return True

        self.progress_bar.set_fraction(0.0)
        self.progress_bar_lock.release()
        return False

    def on_button_click(self, widget):
        self.counter += 1
        thread = threading.Thread(target=self.update_label,
                                  args=(self.counter,))
        thread.start()

        if self.progress_bar_lock.acquire(False):
            glib.timeout_add(250, self.pulse_progress_bar)


if __name__ == '__main__':
    test = Test()
    gtk.main()

做出的改动有:

  • 在回调函数中不再等待线程完成,这样主循环可以继续处理其他事件。
  • 增加了一个进度条,用来显示线程正在执行时的状态。
  • 使用了 glib.timeout_add 来安排一个回调函数,这个函数会在某个线程执行时让进度条闪烁。这和轮询线程的效果一样,但好处是主循环仍然能响应其他事件。
  • 使用了 threading.Lock 来防止回调函数被多次安排,无论按钮被点击多少次。
  • 增加了 gobject.threads_init,这个在之前的例子中缺失了。

现在,当你点击按钮时,你会看到标签被点击,进度条也会闪烁,只要线程在运行。

撰写回答