Multi-Threaded Programming
- Initializing GLib in thread-safe
mode.
- A minimal threaded Inti application.
- Creating a thread.
- Joining threads.
- Mutexes.
- Conditions.
- Thread Example.
- Thread pitfalls.
Most programmers are used to writing
single-threaded
programs - that is, programs that only execute one path through
their
code at a time. Multi-threaded programs may have several threads
running
through different code paths simultaneously. One of the
advantages of multi-threaded programming is that it's considerably
cheaper to switch
between two threads in a single process than to switch between two
processes. Another advantage is that threads can often improve the
performance of a program without incurring significant overhead to
implement. Be warned though, writing multi-threaded programs requires
careful thought. There is the potential to introduce subtle timing
faults, or faults caused by the unintentional sharing of variables.
Also, debugging a multi-threaded program is much harder than a
single-threaded one .
To understand threads just think of several processes running at once.
Imagine that all these processes have access to the same set of global
variables and function calls. Each of these processes would represent a
thread of execution and is thus called a thread. The important
differentiation is that each thread doesn't have to wait for any other
thread to proceed. All the threads can proceed simultaneously. Unlike
processes all threads of
one process share the same memory. This is good, as it provides easy
communication between the involved threads via this shared memory, and
it is bad, because strange things might happen, when the program is not
carefully designed.
The main benefit of multi-threading a graphical user interface is
increased responsiveness to user requests. One of the more frustrating
aspects of both programming and using applications with graphical user
interfaces is dealing with operations that take an indeterminate amount
of time. Using threads in such an application provides at minimum a
more responsive interface and perhaps one that permits more work to
occur by allowing the user to queue possible multiple long-running
requests.
Thread operations include thread creation, termination, synchronization
(joins, blocking), scheduling, data management and process interaction.
A thread does not maintain a list of created threads, nor does it know
the thread that created it. All threads within a process share:
- The same address space
- Process instructions
- Most data
- open files (descriptors)
- signals and signal handlers
- current working directory
- User and group id
Through GLib encapsulation, Inti
provides a portable means for writing
multi-threaded software. It provides mutexes to protect access to
portions of memory
(G::Mutex, G::StaticMutex, G::StaticRecMutex and G::StaticRWLock), a
condition object for condition variables that allow threads to be
synchronized (G::Condition) and finally thread-private data objects
that every thread has a private instance of
(G::Private, G::StaticPrivate). Last but definitely not least there is
the thread object itself, to portably create and manage threads
(G::Thread).
Initializing GLib in thread-safe
mode
The first thing that must be done when writing a multi-threaded program
is to initialize GLib in thread-safe mode. This is done by calling the
following method.
G::Thread::init() must be called
before executing any other GTK+ or GDK
functions in
a threaded GTK+ program. For convenience, G::Thread::init() calls both
g_thread_init() and
gdk_threads_init(). In thread-safe mode
GLib
automatically locks all internal data structures as needed. This does
not mean that two threads can simultaneously access the same data, but
they can access two different instances of the data simultaneously. If
two different threads need to access the same data, the application is
responsible for locking itself.
GLib is completely thread safe (all global data is automatically
locked), but individual data structure instances are not automatically
locked for performance reasons. So you must coordinate accesses to
the same data from multiple threads. GTK+ is "thread aware" but
not thread safe, so Inti provides a global lock controlled by
Gdk::Mutex::lock() and Gdk::Mutex::unlock() which protects all use of
GTK+. That is, only one thread can use GTK+ at any given time.
Idles, timeouts, and input signals are executed outside of the main
GTK+ lock. So, if you need to call GTK+ inside of such a callback slot,
you must surround the callback with a Gdk::Mutex::lock() and
Gdk::Mutex::unlock() pair. (However, all other signals are still
executed within the main GTK+ lock.) In particular, this means, if you
are writing widgets that might be used in threaded programs, you must
surround timeouts and idle functions in this matter. As always, you
must also surround any calls to GTK+ not made within a signal handler
with a Gdk::Mutex::lock() and Gdk::Mutex::unlock() pair.
Before calling Gdk::Mutex::unlock() from a thread other than your main
thread, you probably want to call Gdk::flush() to send all pending
commands to the windowing system. (The reason you don't need to do this
from the main thread is that GDK always automatically flushes pending
commands when it runs out of incoming events to process and has to
sleep while waiting for more events.)
A minimal threaded Inti application
A minimal main program for a threaded Inti application looks like this:
#include
<inti/main.h>
#include <inti/gtk/window.h>
#include <inti/glib/thread.h>
using namespace Inti;
class Window : public Gtk::Window
{
public:
Window();
virtual
~Window();
};
Window::Window()
{
set_title("Basic
Window");
show();
}
Window::~Window()
{
}
int main (int argc, char *argv[])
{
using namespace
Main;
if
(!G::Thread::supported())
G::Thread::init();
init(&argc, &argv);
Window window;
window.sig_destroy().connect(slot(&Inti::Main::quit));
Gdk::Mutex::lock();
run();
Gdk::Mutex::unlock();
return
0;
}
|
This example doesn't do much but it
does show you how to correctly
initialize GLib in thread-safe mode, and how to lock the GTK+ main loop
(that is, run()). If G::Thread::init() is called twice, the second time
it will abort. To make sure this doesn't happen G::Thread::supported()
should be checked first.
bool G::Thread::supported() const; |
The G::Thread supported() method returns false if the GLib thread
system has not yet been initialized, and true if it has.
Creating a thread
Creating a thread in Inti is easy
because unlike other C++ thread
implementations, your not required to derive a new class or override
any virtual functions. Instead G::Thread provides a static
function-call API that lets to create threads on-the-fly, in any
constructor or function body.
To create a new thread you call one of the following methods:
static Thread* create(const ThreadSlot *slot, bool joinable, G::Error *error =
0);
static Thread* create(const ThreadSlot *slot, unsigned long stack_size, bool joinable, bool bound, G::Error *error = 0);
|
Usually you should use the first create method, not the second.
The ThreadSlot argument is a typedef that declares the
signature of the callback slot (or entry point) to execute in the new
thread.
typedef Slot0<void> ThreadSlot;
|
The thread slot can be a member or non-member function and has the form:
The joinable argument sets
whether the new thread should be
joinable or not. A join is performed when you want to wait for a thread
to finish. A thread calling routine may launch multiple threads then
wait for them to finish to get the results.
The stack_size and bound arguments are seldom used
and best left to those who know what they're doing. The stack_size
specifies a stack size for the new thread and bound sets
whether the new thread should be bound to a system thread. The G::Error
argument is optional and is only set when the create() method returns
null.
To create a new thread and check for an error you could do something
like this:
#include <iostream>
G::Thread *thread = G::Thread::create(slot(this,
&SomeClass::thread_method), true);
if (!thread)
{
std::cout << "Thread creation failed" <<
std::endl;
}
|
Joining threads
Joining is one way to accomplish synchronization between
threads. Two other ways, mutexes and condition variables will be
discussed later.
To join a thread you call the following method:
The join() method blocks the calling
thread until the specified thread
terminates. As a recommendation, if a thread requires joining it must
be explicitly created as joinable. If you know in advance that a thread
will never need to join with another thread, consider creating it in a
detached state (joinable = false).
To wait for a threads completion you would do something like this:
#include <iostream>
G::Thread *thread = G::Thread::create(slot(this,
&SomeClass::thread_method), true);
if (!thread)
{
std::cout << "Thread creation failed" <<
std::endl;
}
std::cout << "Waiting for the thread to finish..." <<
std::endl;
thread->join();
std::cout << "Thread joined!" << std::endl;
|
Mutexes
Mutex is an abbreviation for "mutual
exclusion". Mutex variables are
one of the primary means of implementing thread synchronization and for
protecting shared data when multiple writes can occur. A mutex variable
acts like a "lock" protecting access to a shared data resource. The
basic concept of a mutex as used in Inti is that only one thread can
lock (or own) a mutex variable at any given time. Thus, even if several
threads try to lock a mutex only one thread will be successful. No
other thread can own that mutex until the owning thread unlocks that
mutex. This ensures that threads take turn in accessing protected data.
To prevent data corruption it is important to make sure that every
thread that needs to use a mutex does so.
There are two groups of mutexes. The first group includes G::Mutex,
G::RecMutex and G::RWLock. These mutexes are used when you want to
dynamically create a mutex on the heap or on the stack. G::Mutex is the
standard mutex and the one
from this group that you will use the most. G::RecMutex is a recursive
mutex that can be locked by the same thread multiple times, but before
it can be locked by other threads it must be unlocked the same number
of times. G::RWLock is a mutex that implements two types of locks, a
read-only lock and a write-lock. A read-write lock has a higher
overhead than the other mutexes.
The second group of mutexes are analogous to the first but must be
created at compiled time, which is sometimes convenient. The names of
these mutexes are prefix with static and include
G::StaticMutex, G::StaticRecMutex and G::StaticRWLock. These mutexes
can be initialized in file scope in an anonymous namespace like this:
G::StaticMutex
mutex = INTI_STATIC_MUTEX_INIT;
G::StaticRecMutex rec_mutex =
INTI_STATIC_REC_MUTEX_INIT;
G::StaticRWLock rw_lock =
INTI_STATIC_RW_LOCK_INIT;
|
The three methods used with mutexes
are lock(), trylock() and unlock().
The trylock() and unlock() methods are the same for all mutexes. The
lock() method for some mutexes is different because you can optionally
specify
an argument. For example, the lock() method for G::RecMutex and
G::StaticRecMutex looks like this:
void lock(unsigned int depth = 1);
|
The depth argument is for
convenience. It lets you specify at
lock time the depth, or number of unlocks that must be performed to
completely unlock a recursive mutex. You should consult the Inti
reference documentation or have a look at the header file <inti/glib/mutex.h>
for more details.
Conditions
The condition variable mechanism
allows threads to suspend execution
and relinquish the processor until some condition is true. A condition
variable must always be associated with a mutex to avoid a race
condition created by one thread preparing to wait and another thread
which may signal the condition before the first thread actually waits
on it resulting in a deadlock. The thread will be perpetually waiting
for a signal that is never sent. Any mutex can be used, there is no
explicit link between the mutex and the condition variable.
The following is an example of using G::Condition to block a thread
until a condition is satisfied.
G::Condition
*data_cond = 0; //
Must be initialized somewhere.
G::Mutex *data_mutex = 0; // Must
be initialized somewhere.
void *current_data =
0;
void push_data(void *data)
{
data_mutex->lock();
current_data = data;
data_cond->signal();
data_mutex->unlock();
}
void* pop_data()
{
data_mutex->lock();
while
(!current_data)
data_cond->wait(*data_mutex);
void
*data = current_data;
current_data = 0;
data_mutex->unlock();
return
data;
}
|
Thread Example
The following thread example is a C++
translation of the GTK+ thread
example in the GTK+ FAQ sheet. It's a simple GUI application that
displays a window whose only widget is a label. When the program is
first run the label displays the text "And now for something completely
different ...". The program then creates two threads that argue with
each
other, one thread setting the label text to "Oh yes, it is!"
and the other thread setting the label
text to "Oh no, it
isn't!"
Between calls to the thread slot
on_argument() each thread sleeps for a
random amount of time. When a thread is woken up it resets the label
text if the current text was set by the other thread. And so
the two threads argue with each other, over and over again, resetting
the label's text.
The header file for
Thread Example is:
#include <inti/main.h>
#include <inti/gtk/window.h>
#include <inti/gtk/label.h>
using namespace Inti;
class Window : public Gtk::Window
{
static
volatile int yes_or_no;
Gtk::Label *label;
protected:
void
on_argument(int what);
public:
Window();
virtual
~Window();
};
|
and the source file for Thread Example is:
/* Inti threads example.
*
* Based on the
GTK threads example in the GTK+ FAQ sheet
* Copyright (C)
1999, Erik Mouw, <J.A.K.Mouwits.tudelft.nl>
*/
#include "thread.h"
#include <inti/glib/rand.h>
#include <inti/glib/thread.h>
#include <inti/bind.h>
using namespace Inti;
namespace {
G::StaticMutex mutex = INTI_STATIC_MUTEX_INIT;
const int YES_IT_IS =
1;
const int
NO_IT_IS_NOT = 0;
} // namespace
volatile int
Window::yes_or_no = YES_IT_IS;
Window::Window()
{
set_title("Thread Example");
set_border_width(10);
// create a
label
label = new
Gtk::Label("And now for something completely different ...");
add(*label);
label->show();
// init random
number generator
G::random_set_seed((unsigned int)time(0));
// create the
threads
G::Thread *thread = G::Thread::create(bind(slot(this, &Window::on_argument),
YES_IT_IS), false);
thread = G::Thread::create(bind(slot(this, &Window::on_argument),
NO_IT_IS_NOT), false);
show();
}
Window::~Window()
{
}
void
Window::on_argument(int
what)
{
bool
say_something;
for (;;)
{
// sleep for while
G::usleep((G::random_int() /
(RAND_MAX / 3) + 1) * (G_USEC_PER_SEC / 2));
// lock the yes_or_no_variable
mutex.lock();
// do we have to say something?
say_something = (yes_or_no !=
what);
if (say_something)
{
// set the variable
yes_or_no =
what;
}
// unlock the yes_or_no variable
mutex.unlock();
if (say_something)
{
// lock the GTK thread
Gdk::Mutex::lock();
// set the label text
if(what == YES_IT_IS)
label->set_text("Oh yes, it is!");
else
label->set_text("Oh no, it isn't!");
// flush and unlock the GTK thread
Gdk::flush();
Gdk::Mutex::unlock();
}
}
}
int main (int argc, char *argv[])
{
using namespace
Main;
// init thread
support
if
(!G::Thread::supported())
G::Thread::init();
// init GTK+
init(&argc, &argv);
// create a
window
Window window;
window.sig_destroy().connect(slot(&Inti::Main::quit));
// enter the
main loop
Gdk::Mutex::lock();
run();
Gdk::Mutex::unlock();
return
0;
}
|
The Thread Example source code can be found in the <examples/thread>
directory. There are also several small test applications in the <tests/thread>
directory.
Thread Pitfalls
Race conditions.
While the code may appear on the
screen in the order you wish the code to execute, threads are scheduled
by the operating system and are executed at random. It cannot be
assumed that threads are executed in the order they are created. They
may also execute at different speeds. When threads are executing
(racing to complete) they may give unexpected results (race condition).
Mutexes and joins must be utilized to achieve a predictable execution
order and outcome.
Thread safe code.
The threaded routines must call functions which are "thread safe". This
means that there are no static or global variables which other threads
may clobber or read assuming single threaded operation. If static or
global variables are used then mutexes must be applied or the functions
must be re-written to avoid the use of these variables. In C/C++, local
variables are dynamically allocated on the stack. Therefore, any
function that does not use static data or other shared resources is
thread-safe. Thread-unsafe functions may be used by only one thread at
a time in a program and the uniqueness of the thread must be ensured.
Many non-reentrant functions return a pointer to static data. This can
be avoided by returning dynamically allocated data or using
caller-provided storage. An example of a non-thread safe function is
strtok which is also not re-entrant. The "thread safe" version is the
re-entrant version strtok_r.
Mutex Deadlock. This condition occurs when a mutex is
applied but then not "unlocked". This causes program execution to halt
indefinitely. It can also be caused by poor application of mutexes or
joins. Be carefull when applying two or more mutexes to a section of
code. If the first G::Mutex::lock() is applied and the second
G::Mutex::lock() fails due to another thread applying a mutex, the
first mutex may eventually lock all other threads from accessing data
including the thread which holds the second mutex. The threads may wait
indefinitely for the resource to become free causing a deadlock. It is
best to test by calling G::Mutex::trylock() and if failure occurs, free
the resources and stall before retrying.
References
and further reading