PyGTK, Multithreading and progress bar
Multithreading in Python seems to be very simple. Let’s look at the example:
import threading import time global_var = 0 class ThreadClass(threading.Thread): def run(self): global global_var while True: print global_var class ThreadClass2(threading.Thread): def run(self): global global_var while True: time.sleep(1) global_var += 1 t1 = ThreadClass() t1.start() t2 = ThreadClass2() t2.start() t1.join() t2.join()
Thread 1 is printing the global_var and Thread 2 is incrementing it by 1, every second.
Recently I needed to use threads in PyGTK to display progress bar. Off course the state of progress bar was dependent on the other thread. It had to be updated after each subtask call. This is the code:
import threading import time import gtk def init_progressbar(): main_box = gtk.VBox() progressbar = gtk.ProgressBar() progressbar_box = gtk.HBox(False, 20) main_box.pack_start(progressbar_box, False, False, 20) progressbar_box.pack_start(progressbar) info_box = gtk.VBox() main_box.pack_start(info_box, False, False, 10) info_label = gtk.Label("Running...") info_box.pack_start(info_label) return main_box, progressbar, info_label def run_tasks(pb, info_label): task_list = ['task1', 'task2', 'task3', 'task4'] task_no = 0 for task in task_list: pb.set_fraction(float(task_no)/len(task_list)) run_task(2, task, info_label) task_no += 1 pb.set_fraction(float(task_no)/len(task_list)) info_label.set_text('Finished') def run_task(delay, task, info_label): info_label.set_text(task + ' is running...') time.sleep(delay) # simulation of running the task win_pb, pb, info_label = init_progressbar() win = gtk.Window() win.set_default_size(400,100) win.add(win_pb) win.show_all() win.connect("destroy", lambda _: gtk.main_quit()) t = threading.Thread(target=run_tasks, args=(pb,info_label)) t.start() gtk.main()
And…it was not working. The progress bar (and the label) did not get updated after task calls. Why?
It was caused by the fact, that PyGTK is not thread safe. Fortunately we have function threads_init() in gobject module, which can handle it. We just need to call gobject.threads_init() before first thread creation. Corrected (working) code looks like that:
import threading import time import gobject import gtk def init_progressbar(): main_box = gtk.VBox() progressbar = gtk.ProgressBar() progressbar_box = gtk.HBox(False, 20) main_box.pack_start(progressbar_box, False, False, 20) progressbar_box.pack_start(progressbar) info_box = gtk.VBox() main_box.pack_start(info_box, False, False, 10) info_label = gtk.Label("Running...") info_box.pack_start(info_label) return main_box, progressbar, info_label def run_tasks(pb, info_label): task_list = ['task1', 'task2', 'task3', 'task4'] task_no = 0 gobject.threads_init() for task in task_list: pb.set_fraction(float(task_no)/len(task_list)) run_task(2, task, info_label) task_no += 1 pb.set_fraction(float(task_no)/len(task_list)) info_label.set_text('Finished') def run_task(delay, task, info_label): info_label.set_text(task + ' is running...') time.sleep(delay) # simulation of running the task win_pb, pb, info_label = init_progressbar() win = gtk.Window() win.set_default_size(400,100) win.add(win_pb) win.show_all() win.connect("destroy", lambda _: gtk.main_quit()) t = threading.Thread(target=run_tasks, args=(pb,info_label)) t.start() gtk.main()
I wanted also to be able to cancel program computation, by the cancel button. To do that I needed shared variable to be a status flag informing, whether computation should be cancelled or continued. It had to be accessible in task loop and cancel event.
In Python there are mutable and immutable objects. Mutable are passed by reference, but immutable – by value. More about that on this StackOverflow question. Boolean type is immutable, but there is a way around this: use list (mutable type) with one element (boolean variable). I know it is awful solution, but I did not find any better. If you know one, let me know!
This is the app with cancel button:
import threading import time import gobject import gtk def init_progressbar(): main_box = gtk.VBox() progressbar = gtk.ProgressBar() progressbar_box = gtk.HBox(False, 20) main_box.pack_start(progressbar_box, False, False, 20) progressbar_box.pack_start(progressbar) info_box = gtk.VBox() main_box.pack_start(info_box, False, False, 10) info_label = gtk.Label("Running...") info_box.pack_start(info_label) cancel_box = gtk.HBox() info_box.pack_start(cancel_box) cancel_button = gtk.Button("Cancel") cancel = [False] cancel_button.connect("clicked", cancel_counting, info_label, cancel) cancel_box.pack_start(cancel_button, False, False, 20) return main_box, progressbar, info_label, cancel def run_tasks(pb, info_label, cancel): task_list = ['task1', 'task2', 'task3', 'task4'] task_no = 0 gobject.threads_init() for task in task_list: if cancel[0]: pb.set_fraction(0) info_label.set_text('Canceled') return pb.set_fraction(float(task_no)/len(task_list)) run_task(2, task, info_label) task_no += 1 pb.set_fraction(float(task_no)/len(task_list)) info_label.set_text('Finished') def run_task(delay, task, info_label): info_label.set_text(task + ' is running...') time.sleep(delay) # simulation of running the task def cancel_counting(widget, info_label, cancel): cancel[0] = True info_label.set_text("Cancelling...") win_pb, pb, info_label, cancel = init_progressbar() win = gtk.Window() win.set_default_size(400,100) win.add(win_pb) win.show_all() win.connect("destroy", lambda _: gtk.main_quit()) t = threading.Thread(target=run_tasks, args=(pb,info_label, cancel)) t.start() gtk.main()
To make above code working on your machine you need to have Python and PyGTK installed. To find out the details, how to install Python and/or PyGTK check my Python jump start post.