Interfacing Threaded Programming with Gtk+
Version 0.1
Last updated: 25-08-04
Introduction
Threaded programming with Gtk+ turns your application more efficient and responsive. But, because the Gtk+ library is not thread safe, interfacing it with multi threaded code has to follow some simple procedures. Also in order for it to be effective, good communication with the user is essential.
This article will try to describe how to develop multithreaded applications which use the Gtk+ toolkit in an Model-View-Controller (MVC) way. It will also provide some guidelines with the respective examples on how to improve the user experience with respect to this matter.
MVC
MVC is, amongst other things, a way of designing your application in a structured way. Everything you design should work in an abstract way, independently of it's presentation. This is why whenever you create an application you should consider developing a command line interface as well as a graphical user interface. This way of thinking does not mean that you actually will implement one of them but instead decouples your application from the presentation layer. This also helps threaded application work interact efficiently with Gtk+ interfaces.
The MVC pattern is also used in Gtk+ library itself. If you read the GtkTreeView
documentation you'll see it mentioned and it's modularity enables the use of an
unique model (the TreeStore or ListStore) with a GtkTreeView or a GtkComboBoxEntry.
Example 1: Background is The Way
Let's imagine we're dealing with an application that is working on a lengthy computation. Threaded programming helps your applications' graphical interface run smoothly while your computation is running in the background.
gboolean lengthy_func_done(gpointer data)
{
GtkWidget *dialog;
dialog = gtk_message_dialog_new(NULL,
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
"Operation completed!");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
return FALSE;
}
gpointer async_lengthy_func(gpointer data)
{
lengthy_func();
g_idle_add(lengthy_func_done, NULL);
return NULL;
}
void on_clicked(GtkWidget *buton, gpointer data)
{
g_thread_create(async_lengthy_func, NULL, FALSE, NULL);
}
When the user clicks the button the function on_clicked is called. When
this event happens we create a new thread, with
g_thread_create.
The thread's body is function async_lengthy_func.
NOTE: Every graphical related call must be performed inside the Gtk+ main
loop. There are mechanisms for running callback functions inside it,
i.e. using
g_idle_add.
After lengthy_func is done, inside the new thread, we ask Gtk+ main loop to
run another callback which will take care of warning the user we're finished.
We used g_idle_add for routing our callback inside the main loop and the
callback used was lengthy_func_done.
Finally, when Gtk+ main loop has an opportunity lengthy_func_done is called and then
we show the message box informing the user that the operation was done.
Example 2: One At a Time
The way our first example was implemented, if the user clicks more then once
the button, lengthy_func is called concurrently those many times.
If we want to avoid this behaviour we must disable this opportunity from the
user.
One way of blocking concurrent calls is to turn the button insensitive, this is a simple, yet effective way of solving the problem, but it is not advised on tasks that take a bit more then 3 seconds, because the user communication is minimal.
gboolean lengthy_func_done(gpointer data)
{
GtkWidget *dialog;
dialog = gtk_message_dialog_new(NULL,
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
"Operation completed!");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
/* enable our button again */
gtk_widget_set_sensitive(GTK_WIDGET(data), TRUE);
return FALSE;
}
gpointer async_lengthy_func(gpointer data)
{
lengthy_func();
g_idle_add(lengthy_func_done, data);
return NULL;
}
void on_clicked(GtkWidget *buton, gpointer data)
{
/* disable our button */
gtk_widget_set_sensitive(button, FALSE);
g_thread_create(async_lengthy_func, button, FALSE, NULL);
}
Before creating the working thread we turn the button insensitive. We send the button as user data through our many callbacks. When the callback that warns the user is called, we warn the user and then turn our button sensitive again.
Example 3: Let's Be User Friendly
When your computation lasts from 3~5 seconds and you cannot provide a progress indication you should at least present the user a dialog which warns him that it will take a while before the computation finishes. To do so we create a modal window which disables all access to parent window. Since the parent window is not accessible we don't need to turn the button insensitive, this corresponds to our first change in the code:
/* we no longer need to send the button as user data */ g_signal_connect(button, "clicked", G_CALLBACK(on_clicked), NULL);
Now after the button is clicked the modal window must be created:
void on_clicked(GtkWidget *button, gpointer data)
{
/* disable our button */
GtkWidget *win, *label;
GdkCursor *cursor;
win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_modal(GTK_WINDOW(win), TRUE);
gtk_window_set_title(GTK_WINDOW(win), "Progress");
gtk_container_set_border_width(GTK_CONTAINER(win), 12);
g_signal_connect(win, "delete_event", G_CALLBACK(gtk_true), NULL);
label = gtk_label_new("Please wait a little...");
gtk_widget_show(label);
gtk_container_add(GTK_CONTAINER(win), label);
gtk_widget_show(win);
cursor = gdk_cursor_new(GDK_WATCH);
gdk_window_set_cursor(win->window, cursor);
gdk_cursor_unref(cursor);
g_thread_create(async_lengthy_func, win, FALSE, NULL);
}
The window only contains a label that should explain the user he should wait a bit. To increase the visual feedback we also change the mouse cursor, on that window, to the watch.
We need to send the window we created as user data because it will be destroyed afterwards:
GtkWidget *w; w = GTK_WIDGET(data); gtk_widget_hide(w); gtk_widget_destroy(w);
Example 4: Let's Progress
Computations that take more then 5 seconds must have a dialog with a progress bar
indicating the user that the application did not hang.
Calling lengthy_func there is no way to know it's progress, therefore we
must use a pulse only progress. A pulse progress bar simply shows when there
is some activity, because we cannot know that either we'll do this in a
repetitive timed event. Remembering our golden rule to not change Gtk+ related
state if we're not in the main loop we must have this repetitive and timed
loop being run inside Gtk+'s main loop.
The function
g_idle_timeout_add does just that, you can set a callback which
will be called from time to time inside the main loop.
We pick the window of our last example and we add the progress bar. In the
timed event we pulse the progress bar. When we're finished we must stop the
timed task and destroy the dialog.
gboolean update_progress(gpointer data)
{
gtk_progress_bar_pulse(GTK_PROGRESS_BAR(data));
return TRUE;
}
void on_clicked(GtkWidget *button, gpointer data)
{
GtkWidget *win, *w, *vbox;
guint sid;
/* create the modal window which warns the user to wait */
win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_modal(GTK_WINDOW(win), TRUE);
gtk_window_set_title(GTK_WINDOW(win), "Progress");
gtk_container_set_border_width(GTK_CONTAINER(win), 12);
g_signal_connect(win, "delete_event", G_CALLBACK(gtk_true), NULL);
vbox = gtk_vbox_new(FALSE, 12);
gtk_widget_show(vbox);
/* create label */
w = gtk_label_new("Please wait...");
gtk_widget_show(w);
gtk_container_add(GTK_CONTAINER(vbox), w);
/* create progress bar */
w = gtk_progress_bar_new();
gtk_widget_show(w);
gtk_container_add(GTK_CONTAINER(vbox), w);
/* add vbox to dialog */
gtk_container_add(GTK_CONTAINER(win), vbox);
gtk_widget_show (win);
/* refresh the progress dialog */
sid = g_timeout_add(100, update_progress, w);
g_object_set_data(G_OBJECT(win), "source_id", GINT_TO_POINTER(sid));
g_thread_create(async_lengthy_func, win, FALSE, NULL);
}
Before hiding and destroying the dialog we must add this line:
/* stop pulsing progress bar */ g_source_remove(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w), "source_id")));
The first difference from our last example is that we create a
GtkVBox
and add the GtkLabel from the last example and a also a
GtkProgressBar.
After this we create the timed repetitive callback with g_timeout_add. Our
callback update_progress accesses the user data (the progress bar) and sends
a pulse to it.
Whenever we call g_idle_add or g_timeout_add
we are creating something called a
GSource
and attaching it to a
GMainContext.
A GSource is, making it simple, our callback function and it's associated
user data.
A GMainContext, in our case, is Gtk+'s main loop. But when we call one of
these functions the returned value is an ID which identifies the created
GSource. We need this ID to stop the timed event which sends a pulse to
our progress bar.
This is what that last line of code is used for.
The Other Way To Do It
Another way to interface threaded programming and Gtk+ is to issue the graphical
related calls directly from the outside threads. To do this you have to lock
the main loop, by surrounding it with
gdk_threads_enter
and
gdk_threads_leave
. All you Gtk+ related calls must be surrounded with gdk_threads_enter and
gdk_threads_leave.
This way of working is not the advised way to do it because it's error prone, tends to be uneffective and clashes with the program modularity. Gdk explains the why's and how's more throughly in it's Gdk threads section.
Conclusion
One really annoying thing in our examples is that the user does not have the possibility to cancel the progress. When developing your Model, making it cancelable and mesurable (in terms of progress) is user friendly. Users sometimes don't want wait and there are times they simply can't. Disabling this possibility is big drawback in any application.
Resources
The four examples can be downloaded in a tarball gtk_threads.tar.gz.