Threads and gtkmm



Hi,

In order to deal in part with the issues thrown up by the bug report at
https://bugzilla.gnome.org/show_bug.cgi?id=512348 , I have agreed to
contribute a new section to the gtkmm tutorial on writing
multi-threaded programs using gtkmm.  This will reduce the risk of too
many people writing thread unsafe code because of the (partly hidden)
problems associated with libsigc++.

The text set out below (which for ease of reading is at this stage in
plain text rather than formatted XML) is drawn from my own experience in
writing multi-threaded code and in going through the tedious business of
inspecting libsigc++ in order to determine what is safe and what is
not. However, I know I am not the only person to have been faced with
this so if anyone else has any gotchas which they have come across and
which I have not mentioned, or suggestions arising from their own
experience, I would like to hear about them.

Regards,

Chris


***********************

Writing multi-threaded programs using gtkmm

The constraints

Programs employing multiple threads of execution can be written using
gtkmm, as glibmm wraps the functions and objects provided by glib for
threads, and so provides the normal set of thread launching functions,
mutexes, condition variables and scoped locking classes required for
writing multi-threaded programs using C++. Those using recent versions
of g++'s C++0x implementation can also make use of the thread
facilities to be provided by the forthcoming C++11/12 standard, in
combination with glibmm and gtkmm.

However, care is required arising from the fact that libsigc++ is not
thread-safe.  Amongst other things, a class inheriting from
sigc::trackable will, via that inheritance, have a std::list object
keeping track of slots connected to any of its non-static methods.
Each sigc::slot object also keeps, via sigc::slot_rep, its own
sigc::trackable object to track any sigc::connection objects which it
needs to inform about its demise.  sigc::signal objects also keep
lists of slots, which will be updated with a call to its connect()
method or calls to any sigc::connection object relating to such a
connection.  None of these are protected by mutexes or other means of
synchronization.

This requires a number of rules to be observed when writing
multi-threaded programs using gtkmm:

1.  Although code written for the X11 backend of GTK+ can use the GDK
global lock, as invoked by gdk_threads_enter() and
gdk_threads_leave(), so as to address GTK+ in more than one thread,
because of libsigc++ this will not usually work with gtkmm.  Instead,
if a worker thread needs to invoke a gtkmm function, it should do so
via Glib::Dispatcher.  Glib::Dispatcher is the key to writing
successful multi-threaded code using glibmm and gtkmm, and is dealt
with in more detail below.  (As it happens, this approach also provides
a cleaner program structure than using the global lock.)

2.  Unless special additional synchronization is employed, any
particular sigc::signal object should be regarded as 'owned' by the
thread which created it.  Only that thread should connect slots with
respect to the signal object, and only that thread should emit() or
call operator()() on it.

3.  Unless special additional synchronization is employed, any
sigc::connection object should be regarded as 'owned' by the thread
which created the slot and called the method which provided the
sigc::connection object.  Only that thread should call
sigc::connection methods on the object.

4.  A slot which references a non-static method of a class deriving
from sigc::trackable should never be copied to another thread (one
consequence of this is that Glib::Thread::create() should not be
called with a slot argument which represents a non-static method of
such a class).

5.  If a particular class object derives from sigc::trackable, only
one thread should create slots representing any of its non-static
methods (that is, create slots with sigc::mem_fun()).  The first
thread to create such a slot should be regarded as owning the relevant
object for the purpose of creating further slots referencing ANY of
its non-static methods.

6.  Although glib is itself thread-safe (assuming Glib::thread_init()
has been called), any glibmm wrappers which use libsigc++ will not be.
So for example, the only thread which should call
Glib::SignalIdle::connect(), Glib::SignalIdle::connect_once(),
Glib::SignalIO::connect(), Glib::SignalTimeout::connect(),
Glib::SignalTimeout::connect_once(),
Glib::SignalTimeout::connect_seconds,
Glib::SignalTimeout::connect_seconds_once(), and manipulate any
sigc::connection object returned by them, is the thread in which the
main loop to which the connection is made runs.  (The connect*_once()
variants could be made thread-safe for any case where the slot does
not relate to a non-static method of a class deriving from
sigc::trackable, but that has not yet been implemented.)

Point 5 above is particularly unintuitive.  The overall message is:
take extra care when deriving classes from sigc::trackable in
multi-threaded programs.

Using Glib::Dispatcher

The slots connected to sigc::signal objects execute in the thread
which calls emit() or operator()() on the signal.  Glib::Dispatcher
does not behave this way: instead its connected slots execute in the
thread in which the Glib::Dispatcher object was constructed (which
must have a glib main loop).  If a Glib::Dispatcher object is
constructed in the main GUI thread (which will therefore be the
receiver thread), any worker thread can emit on it and have the
connected slots safely execute gtkmm functions.

Some thread-safety rules on the use of Glib::Dispatcher still apply:
only the receiver thread (normally the main GUI thread) should call
connect() on it, or manipulate any related sigc::connection object,
unless additional synchronization is employed.  However, any worker
thread can safely emit on it without any locking once the receiver
thread has connected the slots, provided that the Glib::Dispatcher
object is constructed before the worker thread is started (if it is
constructed after the thread has started, additional synchronization
will normally be required to ensure visibility).

Aside from the fact that connected slots always execute in the receiver
thread, Glib::Dispatcher objects are similar to sigc::signal<void>
objects.  They therefore cannot pass unbound arguments nor return a
value.  The best way to pass unbound arguments is with a thread-safe
(asynchronous) queue.  At the time of writing glibmm does not have one,
although most people writing multi-threaded code will have one
available to them (they are relatively easy to write although there are
subtleties in combining thread-safety with strong exception safety).

A Glib::Dispatcher object can be emitted on by the receiving thread as
well as by a worker thread, although this should be done within
reasonable bounds.  Glib::Dispatcher objects share a single common
pipe, which could in theory at least fill up on a very heavily loaded
system running a program with a very large number of Dispatcher
objects in use.  Were the pipe to fill up before the receiving
thread's main loop has had an opportunity to read from it to empty it,
and the receiving thread attempt to emit and so write to it when it is
in that condition, the receiving thread would block on the write, so
deadlocking.  Where the receiving thread is to emit, a normal
sigc::signal<void> object can of course be used instead.





[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]