Multithreading with GTK
The rule that governs GTK multithreading is the one every GUI toolkit shares: only the main thread - the thread that runs the gtk.main() loop - may touch the widgets. The work that takes time (the file scan, the network call, the computation) runs in a worker thread, and the result is marshaled back to the main thread before it touches the UI. The marshaling tools are GLib's idle_add (run a function on the main thread when the loop is idle) and timeout_add (run it on a timer), both of which schedule the function on the main thread's loop from any thread. This chapter covers the rule and its reason (the widget is not thread-safe), the worker-thread pattern (the work off-thread, the result marshaled), the idle_add and timeout_add as the marshaling, the progress reporting, and the shutdown discipline.
The Rule: Only the Main Thread Touches Widgets
import gtk
# the widget is NOT thread-safe: the toolkit's object model
# assumes a single owner (the main thread, the loop's thread)
# a worker thread that calls label.set_text() races the main
# thread's render of the same label - the corruption is the
# intermittent crash, the garbled text, the "sometimes works"
# the structure that is safe:
# main thread - builds the UI, runs the loop, touches widgets
# worker thread - does the work, touches NO widget, posts the
# result to the main thread
# the anti-pattern (the crash that comes "sometimes"):
def bad_worker():
do_long_work()
label.set_text("done") # a WORKER thread touching a
# widget: the race
# the pattern (the safe form):
import gobject
def good_worker():
result = do_long_work()
# marshal to the main thread: the idle runs ON the loop
gobject.idle_add(update_label, result)
return None
def update_label(result):
# this runs on the MAIN thread (the loop's thread):
label.set_text(result) # safe: the widget's owner
return False # False: the idle runs once
The rule is the single-thread ownership: the widgets belong to the main thread (the thread that entered gtk.main()), and only that thread may call their methods. The reason is the toolkit's object model - the widget's state (its text, its allocation, its render) is not protected by locks against concurrent access, and a worker thread calling set_text while the main thread renders the same label is a data race, which is the class of bug that is intermittent (the "sometimes crashes, sometimes garbled" that is the signature of the unsynchronized widget access). The safe structure is the split: the main thread builds the UI and runs the loop and is the only one that touches the widgets, and the worker thread does the long work and touches no widget - it computes the result and posts it to the main thread, which applies it to the UI. The marshal is the bridge, and the next section is the tool.
The Marshaling: idle_add and timeout_add
import gtk
import gobject
import threading
# idle_add(func, *args) - schedule func on the MAIN thread,
# to run when the loop is idle; callable from ANY thread
def update_label(text):
label.set_text(text)
return False # False: do not repeat the idle
def worker():
for i in range(100):
do_one_unit()
# post the progress to the main thread:
gobject.idle_add(update_label, "working %d/100" % (i + 1))
gobject.idle_add(update_label, "complete")
return None
# the launch: the worker in a thread, the loop on the main
t = threading.Thread(target=worker)
t.start()
gtk.main()
# timeout_add(interval_ms, func) - the timer form: the func
# runs on the main thread after the interval (and repeats if
# it returns True) - the same marshaling, on a schedule
def poll():
if worker.has_result():
label.set_text(worker.take_result())
return False # stop the timer
return True # keep polling
# the two are the same mechanism: a function scheduled on the
# main thread's loop, callable from the worker, the function
# itself runs on the main thread (safe for the widgets)
The marshaling is the idle_add (and the timeout_add): a call that schedules a function to run on the main thread's event loop, callable from any thread (the worker), with the scheduled function itself running on the main thread (safe for the widgets). The idle_add(func, *args) runs func when the loop is idle, with the args passed through, and the return value is the repeat flag (False for a one-shot, True to reschedule). The timeout_add(interval, func) is the timer form - the function after the interval, repeating on True - which is the polling shape (the worker's result checked on a timer). The pattern is the two-thread dance: the worker thread does the work and posts each update with an idle_add (the progress text, the final result), and the main thread's loop runs those idles on its own thread, where the widget calls are safe. The discipline: the worker never touches a widget directly - it computes and posts, and the posted function is the one that touches the UI, on the thread that owns it.
The Progress: Reporting from the Worker
import gtk
import gobject
import threading
# the progress bar - updated from the worker through idles:
bar = gtk.ProgressBar()
def set_fraction(frac):
# runs on the MAIN thread (the idle's target):
bar.set_fraction(frac)
return False
def worker(total):
for i in range(total):
do_one_unit()
gobject.idle_add(set_fraction, (i + 1) / float(total))
gobject.idle_add(finished)
return None
def finished():
# the completion: the UI reflects the done state
bar.set_text("Done")
status.set_text("Complete")
return False
# the button that starts the work:
def on_start(button):
button.set_sensitive(False) # no double-launch
t = threading.Thread(target=worker, args=(100,))
t.start()
return None
# the progress is the worker's updates marshaled to the bar:
# each unit posts its fraction, the bar (a widget) is touched
# only by the idle on the main thread, and the completion
# posts the done state the same way
# the indeterminate mode: the work with no known total
# bar.pulse() - the same marshaling (the pulse from the idle)
# for the "working, no percentage" case
The progress reporting is the marshaling applied to the ProgressBar: the worker posts each unit's fraction with an idle_add (the set_fraction function, which runs on the main thread and touches the bar), and the completion posts the done state the same way. The button that starts the work is disabled on launch (the set_sensitive(False), the no-double-launch guard) and the worker thread is started, and the loop keeps running (the main thread is in the loop, the idles are dispatched on it). The set_fraction from 0.0 to 1.0 is the determinate progress, and the bar.pulse() (the same marshaling, the pulse from the idle) is the indeterminate "working" mode for the work with no known total. The structure is the same two-thread pattern with the progress as the posted state: the worker computes and posts, the idle applies to the bar on the main thread, and the bar's updates are smooth because the loop coalesces the idles into its render passes. The progress is the visible proof the worker is running, and it is the marshaling done right.
The Shutdown Discipline
import gtk
import gobject
import threading
# the worker that can be interrupted: the stop flag
stop = threading.Event()
def worker():
i = 0
total = 1000
while i < total and not stop.is_set():
do_one_unit()
i += 1
gobject.idle_add(set_fraction, i / float(total))
if stop.is_set():
gobject.idle_add(cancelled)
else:
gobject.idle_add(finished)
return None
# the window close: signal the worker, then quit the loop
def on_delete(widget, event):
stop.set() # the worker sees it on its next check
# the worker is a daemon (it dies with the process) or the
# app waits for it briefly before the loop exits
gtk.main_quit()
return False
# the thread's lifecycle: the worker is a daemon thread (it
# does not hold the process open) or the app joins it on the
# clean exit - the discipline is the explicit stop, not the
# thread being abandoned
t = threading.Thread(target=worker)
t.daemon = True
t.start()
The shutdown is the discipline for the worker that is running when the app closes: the threading.Event as the stop flag, the worker's loop checking it (the not stop.is_set() in the condition), and the window's delete-event handler setting it before the main_quit - the worker sees the flag on its next iteration, stops, and posts its cancelled state. The thread's lifecycle is the daemon flag (the worker does not hold the process open after the main exits) or the explicit join on the clean shutdown (the app waits for the worker to finish before it exits). The two together - the cooperative stop flag and the daemon or the join - are the shutdown: the worker is not abandoned (it is told to stop, and it stops), and the process is not held open by a lingering worker. The rule extends the main-thread rule to the lifecycle: the main thread owns the loop and the widgets, the worker owns the work and the stop flag, and the shutdown is the main thread signaling the stop and the worker responding on its own thread before the loop exits.
The Working Rules
- Only the main thread touches the widgets. The worker computes and posts; the posted function (the
idle_addtarget) is the one that touches the UI, on the thread that owns it. The direct widget call from the worker is the race. - The
idle_addis the marshal. Callable from any thread, the scheduled function runs on the main thread's loop - the bridge that makes the worker's result safe for the UI. Thetimeout_addis the polling form of the same mechanism. - Post the progress, not the widget call. The worker posts the fraction (the
set_fractionidle), and the bar is touched only by the idle on the main thread - the progress is the marshaling applied to the state. - Disable the launch control on start. The
set_sensitive(False)on the start button is the no-double-launch guard, and the re-enable (from the completion idle) is the restore. - The shutdown is the cooperative stop. The
threading.Eventthe worker checks, set by the close handler before themain_quit, and the daemon flag or thejoinfor the thread's lifecycle - the worker is told to stop, not abandoned.
Summary
The GTK multithreading rule is the single-thread ownership: only the main thread (the gtk.main() loop's thread) may touch the widgets, because the widget's state is not thread-safe and the worker's direct call is the data race behind the intermittent crash. The pattern is the split - the main thread builds the UI and runs the loop and owns the widgets, the worker thread does the long work and touches no widget - and the bridge is the marshaling: the idle_add (and the timeout_add) schedule a function on the main thread's loop, callable from the worker, with the function running on the main thread where the widget calls are safe. The progress is the marshaling applied to the state (the worker posts the fraction, the idle sets the bar), the launch control is disabled on start, and the shutdown is the cooperative stop - the threading.Event the worker checks, set by the close handler, with the daemon flag or the join for the lifecycle. The discipline across it: compute off-thread, post to the main thread, and let the main thread be the only one the widgets see.