An overview of threading in Pike

Pike’s approach to threading is simple and mindful of the sanity of the programmer. Threading is available for all systems using Unix, POSIX, or Windows threads.

The Thread module in the standard library provides a complete system for multi-threading. Initially, a thread is created by instantiating a new instance of the Thread.Thread class with a function that will be executed in a new thread and any arguments to that function:

void say_hello(string name)
{
    write("Hello, " + name + ".\n");
}
 
int main()
{
    Thread.Thread thread;
    thread = Thread.Thread(say_hello, "Jeff");
 
    return -1; // return -1 so execution does not terminate
}

Pike’s interpreter is completely thread-safe. All execution is protected by a global interpreter lock, similar to that of Python. Unlike Python, however, Pike’s locking is finely grained and released often, making threads quite useful in Pike.

Thread.Mutex and Thread.MutexKey

Mutexes are required to protect critical sections that must be executed atomically. Thanks to the interpreter lock, no two threads will ever access the same variable concurrently. However, if a variable is modified or accessed multiple times in a thread, a mutex must be used to guard it:

Thread.Mutex m = Thread.Mutex(); // create a mutex
void func()
{
    Thread.MutexKey k = m->lock(); // acquire the lock
 
    ... // do stuff
    ... // do more stuff
 
    return;
}

Notice that the mutex was never released. Once the MutexKey (returned by Mutex->lock()) goes out of scope, the mutex is automatically released. This means that a method can be made to be synchronized simply by acquiring a lock at the beginning of the function.

Thread.Condition

Condition variables are used to synchronize events across multiple threads. A thread controlling a resource holds a condition variable, with other threads waiting for the condition to be signaled. The controlling thread may chose to signal just one or all waiting threads at once.

A MutexKey must be provided by the waitin thread to guarantee synchronicity with the signaling thread.

Thread.Mutex mutex = Thread.Mutex();
 
void consumer(Thread.Condition c, Thread.Condition c2)
{
    while (1) {
        Thread.MutexKey key = mutex->lock();
        c->wait(key);
        key = 0;
        write("Pong!\n");
        c2->signal();
    }
}
 
void producer(Thread.Condition c, Thread.Condition c2)
{
    while (1) {
        Thread.MutexKey key = mutex->lock();
        c2->wait(key);
        key = 0;
        write("Ping!\n");
        c->signal();
    }
}
 
int main()
{
    Thread.Condition condition1, condition2;
    Thread.Thread prod, cons;
    condition1 = Thread.Condition();
    condition2 = Thread.Condition();
    prod = Thread.Thread(producer, condition1, condition2);
    cons = Thread.Thread(consumer, condition1, condition2);
    condition2->signal(); // get the producer started
    return -1; // keeps script running
}

In this way, a condition variable is akin to a simplified counting semaphore, which may be incremented n times to signal n waiting threads.

Thread.Local

Thread-local storage may be created with Thread.Local:

string thread_fn()
{
    Thread.Local data = Thread.Local();
 
    data->set("foo");
    return data->get();
}

Thread.Queue

Queues are first in, first out containers.
Thread.Queue is expandable, so pushing new elements into the queue will not block:

void consumer(Thread.Queue q)
{
    while (1) {
        string value = q->read(); // block until value available
        do_something_with(value);
    }
}

Another interesting method is read_array, which reads as many values as are available. This is extremely useful. Reading a batch of work results in less time spend blocking, making active queues much more efficient.

Thread.Fifo

The primary difference between the Queue and Fifo classes is that the Fifo class is bounded and will block on writes when full.

void producer(Thread.Fifo q)
{
    string value;
    while (1) {
        value = get_value_from_somewhere();
        q->write(value); // blocks until space is available
    }
}

Conclusion

Pike makes threading easy on programmers. Pike’s internals are thread-safe; a programmer never needs to worry about corrupting memory. Mutually exclusive locks are only used to protect blocks of code that must be executed without interruption, so that only the concurrency of the overall technique must be considered.

Leave a comment | Trackback
Sep 4th, 2008 | Posted in Programming
  1. Mar 5th, 2009 at 05:18 | #1

    Your Thread.Mutex example does not work as you expect it to. If you create the Mutex in the same scope as the lock it is essentially useless. To lock access to some resource you should put the Mutex into the same scope as the resource itself.

  2. Jeff
    Mar 5th, 2009 at 14:43 | #2

    Thanks for noticing the error. I’ve fixed it in the example.

  3. Jeff
    Mar 9th, 2009 at 20:15 | #3

    I also updated the condition example to show how to coordinate with a mutex.