Xfce Foundation Classes
 « Main Page

Multi-Threaded Programming

Table of Contents

  1. Overview
  2. Initializing GTK+ in thread-safe mode.
  3. A minimal threaded XFC application.
  4. Creating a thread.
  5. Joining threads.
  6. Mutexes.
  7. Conditions.
  8. Thread Example.
  9. Thread Pitfalls.
  10. References and further reading

Overview

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 following:
  • 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 XFC 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 GTK+ in thread-safe mode

The first thing that must be done when writing a multi-threaded program is to initialize GTK+ in thread-safe mode. This is done by calling the 'Main::' namespace function:

void threads_init(GThreadFunctions *vtable = 0);

This is a convenience function that initializes the GLib thread system and initializes GDK so that it can be used with multiple threads. There are two parts to the code that this function executes. First, the GLib thread system is initialized:

if (!g_thread_supported())
    g_thread_init(vtable);


If g_thread_init() is called twice, the second time it will abort. To make sure this doesn't happen, g::thread::supported() is checked first. It returns false if the GLib thread system has not yet been initialized, and true if it has.

Second, GDK is initialized so that it can be used in multi-threaded applications:

gdk_threads_init();

Main::threads::init() should only be called once in a threaded GTK+ program, and must be called before executing any other GTK+ or GDK functions. Most of the time you can just pass null for the 'vtable' argument. You should only call this method with a non-null argument if you really know what you are doing. Do not call threads_init() directly or indirectly as a callback and make sure no mutexes are locked when you make the call. After calling threads_init(), either the thread system is initialized or the program will abort if no thread system is available in GLib (that is, G_THREADS_IMPL_NONE is defined).

GTK+ is "thread aware" but not thread safe, so XFC 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. After calling threads_init() you should call Gdk::Mutex::lock() and Gdk::Mutex::unlock() to lock and unlock critical sections of code.

GLib is completely thread safe because it 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. For performance reasons, individual data structure instances are not automatically locked, so if two different threads need to access the same data, the application is responsible for locking itself.

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 (all other signals are 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 manner. 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 XFC application

A minimal main function for a threaded application looks like this:

#include <xfc/main.hh>
#include <xfc/gtk/window.hh>
#include <xfc/glib/thread.hh>

using namespace Xfc;

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;

    threads_init();

    init(&argc, &argv);

    Window window;
    window.signal_destroy().connect(sigc::ptr_fun(&Xfc::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 GTK+ in thread-safe mode, and how to lock the main loop (that is, run()).

Creating a thread

Creating a thread in XFC 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 that lets you create threads on-the-fly, in any constructor or function body.

To create a new thread, 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);


Both methods create a thread with the default priority, but the second method lets you specify a stack size. Usually you would use the first create method.

The ThreadSlot argument is a typedef that declares the function signature of the callback (or entry point) to execute in the new thread:

typedef sigc::slot<void> ThreadSlot;

The thread slot can be a member or non-member function and has the form:

void function();

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:

void G::Thread::join();

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 thread's completion you would do something like this:

#include <iostream>

G::Thread *thread = G::Thread::create(sigc::mem_fun(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 XFC 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 (statically), 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 = XFC_STATIC_MUTEX_INIT;

G::StaticRecMutex rec_mutex = XFC_STATIC_REC_MUTEX_INIT;

G::StaticRWLock rw_lock = XFC_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 XFC reference documentation or have a look at the header file <xfc/glib/mutex.hh> 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; // Initialized somewhere else.
G::Mutex *data_mutex; // Initialized somewhere else.
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.

The header file for the Thread example is <thread.hh>:

#include <xfc/main.hh>
#include <xfc/gtk/label.hh>
#include <xfc/gtk/window.hh>

using namespace Xfc;

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 is <thread.cc>:

#include "thread.hh"
#include <xfc/glib/rand.hh>
#include <xfc/glib/thread.hh>

using namespace Xfc;

namespace {

G::StaticMutex mutex = XFC_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(sigc::bind(sigc::mem_fun(this, &Window::on_argument), YES_IT_IS), false);
    thread = G::Thread::create(sigc::bind(sigc::mem_fun(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!");

            // unlock the GTK thread
            Gdk::flush();
             Gdk::Mutex::unlock();
        }
    }
}

int main (int argc, char *argv[])
{
    using namespace Main;

    // init thread support
    threads_init();

    // init GTK+
    init(&argc, &argv);

    // create a window
     Window window;
    window.signal_destroy().connect(sigc::ptr_fun(&Xfc::Main::quit));

    // enter the main loop
    Gdk::Mutex::lock();
    run();
    Gdk::Mutex::unlock();

    return 0;
}


Compiling Thread

If you compiled and installed XFC yourself, you will find the source code for Thread in the <examples/thread> source directory along with a Makefile. If XFC came pre-installed, or you installed it from an RPM package, you will find the source code in the </usr/share/doc/xfcui-X.X/examples/thread> subdirectory. In this case you will have to create the Makefile yourself (replace X.X with the version number of the libXFCui library you have installed).

To create a Makefile for Thread, add the following lines to a new text file and save it using the name "Makefile":

CC = g++

CFLAGS = -Wall -O2

thread: thread.cc thread.hh
    $(CC) thread.cc -o thread $(CFLAGS) `pkg-config xfcui-X.X --cflags --libs`

clean:
    rm -f *.o thread


If you cut and paste these lines make sure the whitespace before $(CC) and rm is a tab character. When you compile and run this program, a window is appears with a label that 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 text. There are also several small non-GUI threaded test programs in the libXFCcore <tests/thread> subdirectory.

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 (a 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 careful 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



Copyright © 2004-2005 The XFC Development Team Top
XFC 4.4