General TOC
Prev Mach IPC Programming TOC Next

Mach's C Threads

The main abstractions that Mach provides for processes, and communication between them, are task, threads, messages and ports. We already have made an easy programmer's overview to messages and ports; it's time to see now how we can launch threads within a task. Managing threads is a very important thing if we want to exploit parallelism inside our programs and applications.

Mach provides a basic kernel interface for managing threads; is very powerful, and complex too. This is why the usual way for programming threads in Mach is to construct threads packages on top of the kernel's thread interface. The C Threads are one of such packages, and is usually provided in all Mach's implementations.

Another important thing is that C threads can be learned indepently of message passing and ports. Some parts of this chapter are based on cth-hello.tar.bz2

Note : when the word thread is used, we are referring threads in general, not only C threads; for those kind of threads, the cthread word ( naturally ) is used.

Basic C Threads Usage

As explained before, in Mach, tasks are passive entities; its main purpose is to gather the resources associated with a group of threads. The C Threads packages, as implemented in the Hurd, are an easy interface to manage Mach's Threads. The first we must include in a program with cthreads, is, of course :

#include <cthreads.h>

Is usual too, to put, before any cthread related statement :

cthread_init();

Althought usually is not needed. Another important thing is how to declare and initialize cthreads; the first is easy, the second a bit more involved. A cthread declaration is like this :

cthread_t function_thread;

The difficulty of initializing a cthread is in the fact that threads must carry some code; then function that initializes threads is :

cthread_t cthread_fork (any_t (*function)(any_t), any_t arg);

there is something confusing in the declaration above, the new type any_t; it purpose is that we don't need to know what types accept or return some function for a thread to execute its code. Is similar to a void pointer. We could even think about any_t as something like :

typedef void *any_t;

althought the details are not very important. For the moment, with the purpose of learning and getting acustome to cthreads quirks we will use function declarations of the type any_t function( any_t ). As a first example, let's launch a cthread that executes code to print a string; the print function would look like this :

any_t 
print_string( any_t string )
{
	printf( (char *)(string) );

	return( NULL );
}

For assigning/launching the cthread :

cthread_t print_thread;

print_thread = cthread_fork( print_string, "Hello, World\n" );
cthread_join( print_thread );

As you can see a new function have appeared, cthread_join. This function is used to launch a cthread; cthread_fork only "associates" the code we want to execute with some cthread. Another similar function is cthread_detach, that launch some cthread, but with the difference that the program could, if needed, forgot about this cthread and let it vanish. The example function we used before is not a good candidate to see the diference between cthread_join and cthread_detach; but if we would have had as an example a function that returns a value, it would have made the difference. cthread_join would have wait until the return( something );, while cthread_detach maybe not. The declaration of those both functions is :

any_t cthread_join( cthread_t t );
any_t cthread_detach( cthread_t t );

Now that you have understand it, try to write and compile the "hello threads" program. But before that, a warning : pass -lthread to gcc.

Another interesting issue concerning not only cthreads, but also threads in general, is that one of Threadsaveness. Let's explain what is that : threadsaveness is related with what happens when two threads run the same library function at the same time. A library is called thread-safe when both library functions work as if the two calls where invoked in different parts of a threadless program, or by different programs ( remember that two threads is a different situation, since they execute in parallel ); otherway we will call that library thread-unsafe, and the result of the calls will be unpredictable. An example of this could be :

cthread_t Hello, World;

Hello = cthread_fork( print_string, "Hello, " );
World = cthread_fork( print_string, "World\n" );

cthread_join( Hello );
cthread_join( World );

In the code above two separate instances of printf are called in parallel. If the stdlib ( or in general the libc ) is thread safe something like "Hello, World\n" or "World\nHello ," would be obtained. If not we could get anything between a random mix of the strings "Hello, " and "World\n" ( including the disappearing of some characters ) to a segmentation fault. In the past the Hurd libc was thread-unsafe ( the so called "stdio-based" libc ), but actually the libc is thread-safe ( "libio-based" libc ). The first Debian GNU/Hurd CD's set to be "libio-based" was the J series. Debian GNU/Hurd systems before that set were thread-unsafe. In my old G1 and H4 Debian GNU/Hurd systems the result of the above program was :

$./unsafe_hello
HWorld

In my more modern Hurd system, I got the classical Hello, World string. In the next section will be provided an easy solution for those cases when you don't know if a library is thread-safe.

Just few things remain in this basic introduction; one thing is maybe that since C gives to the programmer very much freedom about types and other similar things, it is not necessary to stick to the use of the any_t data type. For example, the next definition of print_string would work ok :

void 
print_string( char *string )
{
	printf( string );

	return;
}

We don't need to make a cast here; it a bit more simple. The poblem comes when you use a function with various argument. In that case is necessary that you work a little more the problem, and use any_t function( any_t ) as some kind of front end adding glue code.

Organizing those Wild Threads

TODO : explain things like mutex_t, mutex_alloc, mutex_free, mutex_lock, mutex_unlock, mutex_trylock, condition_t, condition_alloc, condition_free, condition_signal, condition_wait.

Threads are used to exploit parallelism when writing applications. This can raise some problems, the source of which comes from the fact that in normal situations we have no control of when the execution pass from one threads to others ( in multiprocessors system various threads could be executed at the same time, giving true parallelism ). If, for example, we have some global variable that is accessible from two threads, then it is possible that some unexpected consecuences arises. A NICE EXAMPLE NEEDED.

The situation described above and similar ones are also known as "race conditions". There is a great amount of work, and documentation about them, and is a problem that is not going to be treated in extend here. Some nice presentation and treatment of "race conditions" can be consulted in many OS-related books ( Tanenbaum's book "Operating Systems : Desing and Implementation" is a great place to study this ).

A way to solve this problem with cthreads, and many other threads packages, is declaring a "mutex" ( mutex comes from "mutual exclusion" ). A mutex serves for "locking" some code areas, indicating that this marked code can only be accessed by one thread at the same time. We will explain better in some lines. First, a mutex is declared by this :

mutex_t seal; /* -> since is some kind of seal */

Mutexes are also allocated and freed, like many other resources :

seal = mutex_alloc(); /* allocates a mutex */

mutex_free( seal ); /* frees a mutex */

Now it is time to see how do mutexes solve our above problems. Back to the above example, one solution when some variables are shared between various threads is the mutual exclusion. Mutual exclucion means that at only one thread can access, at some given moment, the shared resources. Then, with this proposal the problem is solved ( althought some new disadvantages may appear ), since the race condition cannot happen. It is necessary also all shared resources in different threads to be marked with the same mutex variable, in this way :

( Code in both cthreads 1 and 2 : )

mutex_lock(seal);
/* code with shared variables and resources */
mutex_unlock(seal);

If the cthread 1 is inside the mutex, and the cthread 2 need to enter "mutexed" code, the cthread 2 will block until the cthread 1 exits the mutex. The use of mutexes, as you can advertise, is simple and effective. One usage of the mutexes could be solve problems with unsafe library functions. In the example mentioned above of printf in old hurd versions, we could avoid any strange behaviour with something like that :

mutex_t unsafe_lib;

any_t 
print_string( any_t string )
{
	mutex_lock(unsafe_lib);
	printf( (char *)(string) );
	mutex_unlock(unsafe_lib);

	return( NULL );
}

In the main program we would have allocated the mutex :

cthread_t Hello, World;

unsafe_lib = mutex_alloc();

Hello = cthread_fork( print_string, "Hello, " );
World = cthread_fork( print_string, "World\n" );

cthread_join( Hello );
cthread_join( World );

mutex_free( unsafe_lib );

Mutexes are itself a way of primitive communication between threads, but their main purpose is to avoid "race conditions" and so, something like "protecting" code.

Coding the Producer-Consumer Problem

Coding the Producer-Consumer Problem with the help of Mach IPC