5

I have a process that will take a while (maybe a minute or two) to complete. When I call this from my pygtk GUI the window locks up (darkens and prevents user action) after about 10 seconds.

I'd like to stop this from happening, but I'm not sure how. I thought multithreading would be the answer, but it doesn't seem to be working. I've tried two different methods I found online. First, I modified this FAQ to take a long running function. Secondly I tried using threading.Thread directly like in this answer, but that also locks up.

My two samples are below. I'm new to multithreading, so maybe it's not the solution I'm looking for. I'm basically just trying to keep the GUI from locking up so I can update with a progress bar and let the user use a cancel button.

#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()
Community
  • 1
  • 1
Kris Harper
  • 5,672
  • 8
  • 51
  • 96

2 Answers2

7

Please find below a modified version of the second example that works for me:

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

The changes made are:

  • Avoid waiting in the callback for the thread to finish to keep the main loop processing events.
  • Added progress bar to display when a thread is being executed.
  • Used glib.timeout_add to schedule a callback that pulses the progress bar when some thread is being executed. This has the same effect as polling the thread, but with the advantage that the the main loop is still responsive to other events.
  • Used threading.Lock to provent the callback to be scheduled more than once, regardless of how many times the button is clicked.
  • Added gobject.threads_init that was missing in this example (not in the previous one).

Now, when clicking on the button, you'll see how the label is clicked and the progress bar pulsed as long as a thread is running.

jcollado
  • 39,419
  • 8
  • 102
  • 133
  • Okay, this makes sense. I understand why I can't constantly poll for the thread's status. But I DO need to know when the thread finishes. If I add a status bar for the thread's progress, how can I know when to stop the status bar? – Kris Harper Dec 21 '11 at 02:25
  • I've added a progress bar to the example. `glib.timeout_add` lets you poll the thread status without making your appplication unresponsive. – jcollado Dec 21 '11 at 09:10
  • Okay great. This makes sense. I didn't know about `glib.timeout_add`. Thanks for your help. – Kris Harper Dec 22 '11 at 01:00
  • Just want to add that the `gobject.threads_init` resolved a similar issue for me where the thread got stuck (for unknown reasons) when trying to return from a sub-function call in the threaded function. – deinonychusaur Oct 04 '12 at 14:15
0

You should reimplement Thread.run for each of your threads, and start a event loop in them.

Also, you could make the button press call the start method for a thread, which will then call run, and do your long task. This way, you don't need an event loop in each thread.

Here is some simple code to explain what I mean for the second option:

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)

I use a callback function because I doubt that set_text is thread-safe.

D K
  • 5,530
  • 7
  • 31
  • 45
  • Sorry, I pasted the same example twice. In the second example, I do have the button call thread.start(), then wait for the thread to finish. This still locks up my application. – Kris Harper Dec 21 '11 at 01:49
  • In the second example you're polling with `thread.is_alive` inside a callback method. That causes that the callback takes as much time to complete as the thread itself and your application isn't processing any other event until the callback terminates. – jcollado Dec 21 '11 at 01:58
  • @d-k I'm interested about the option of running an event loop in each thread because I've never seen that in real code. Could you elaborate on that one? – jcollado Dec 21 '11 at 02:01
  • I am not exactly sure about how it works with GTK, but I know with Qt, you can run a `gobject.MainLoop` inside of a thread in order to process gobject (and most likely GTK) within that thread at the same time as the Qt GUI application runs. – D K Dec 21 '11 at 02:09