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.
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.
Thanks for noticing the error. I’ve fixed it in the example.
I also updated the condition example to show how to coordinate with a mutex.