Tutorial

Inti Logo

« String Handling
 Contributing »

Multi-Threaded Programming

  1. Initializing GLib in thread-safe mode.
  2. A minimal threaded Inti application.
  3. Creating a thread.
  4. Joining threads.
  5. Mutexes.
  6. Conditions.
  7. Thread Example.
  8. 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:
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()

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:

void method_name();

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 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!"

Thread Yes

and the other thread setting the label text to "Oh no, it isn't!"

Thread  No


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



« String Handling
Index
Top
 Contributing »