NEXT UP previous
Next: Calling C Functions

Library Calls

This simple threads library really is very simple, especially in terms of the user interface. Threads are created dynamically at run time, as they are required. This is done with a call to the new_thread() function:

	int new_thread(int (*start_addr)(void), int stack_size)

where start_addr is a pointer to a function which will act, for the newly created thread, like the main() function acts for a process. The stack_size parameter specifies how much space, in bytes, should be set aside for this thread to use as its stack.

Whenever new_thread() is called, a new thread structure is created and initialized and added to a circular doubly linked list of thread structures. This list is used by the thread context switcher (scheduler) to determine which is the next thread to execute when the current thread gives up its control of the processor.

The first time new_thread() is called, usually from within the main() function of a process, the execution of main() itself is suspended and the thread scheduler started up. Only when all the threads created within a particular process have terminated is control finally returned to the main() function, to the point just after the initial new_thread() call.

On all calls to new_thread(), other than the first, a return is made immediately so that the calling thread can continue its execution. The return value from new_thread() is 0 on error (mallocO failure) or 1 on success.

The only other function needed to use this tiny threads library is the call which allows a thread to give up its control of the processor back to the thread scheduler:

	int release (void);

Calling this function will cause the thread scheduler to pass execution control to the next thread in the circular list of thread structures created by new_thread(). Though not covered here, it is a straightforward task to create a simple pre-processor which will automatically add release() statements to your threads code in all the necessary places. To be on the safe side, you should add a release() to your code anywhere where it might loop, so that even an infinite loop will not stop the other threads from being scheduled. Using this criterion, the places that need release() statements are: inside the body of each while, for and do loop, after each program label so that a goto can't cause a loop without a release(), and also at the start of each function so that a recursive function isn't a problem either.

In the situation where two or more threads need to communicate with each other, this can be achieved by the use of variables which are global to the whole process (and therefore to each of its threads). Remember that all the threads within a process share the same program and data memory - their only private data storage area is their stack, which is not used to store global variables.

Communication via global variables is common with threads, but suffers some-what from being asynchronous. This means that without setting up some kind of special arrangement, a data producing thread which writes to a global variable has no way to know that the data has been read by a data consuming thread when it needs to replace the current contents of the variable with the next data value.

In order to make it easier to write threads whose communication is synchronized, the threads library provides a simple communication mechanism between threads, based on the rendezvous concept. The basic idea behind a rendezvous is that it doesn't matter which of the two communicating partners (producer or consumer) arrives at the rendezvous point in its code first; its execution will be suspended until the other partner arrives at the same point. At this time, the data is passed between the two threads, the suspended thread's execution is resumed, and both threads then continue their execution with the communication guaranteed to have taken place.

The threads library provides three function calls related to the rendezvous mechanism: get_channel(), send() and receive().

The get_channel() call has the prototype:

	int get_channel(int number);

Where number is the communication channel number you wish to use, and the return value is a channel descriptor whose value will be passed forward into the other rendezvous related calls (or zero on error). This is rather similar in concept to opening a file except that you use a channel number instead of a file name, and get a channel descriptor returned instead of a file descriptor.

The first get_channel() call on a particular channel number will dynamically create the channel and return its channel descriptor. Subsequent get_channel() requests on the same channel number will just return the channel descriptor of the existing channel, and not create a new one.

The way that channels are implemented allows multiple producers and consumers all to share access to the same channel. In this case, the library will ensure that the producer's messages are properly queued and that each message can only be read by one consumer and will then be removed from the channel, in FIFO order.

Having obtained a channel descriptor, messages can be communicated over the channel with send() and receive():

	int send(int cd, char *buf, int size);
	int receive(int cd, char *buf, int size);

where cd is a channel descriptor returned from a get_channel() call, buf is a pointer to a buffer from which data will be sent or to which data will be received, and size is the size of the data to be transferred, in bytes.

If matching send() and receive() calls specify different data sizes then the smaller of the two will be used. The return values from both the send() and receive() calls will be the actual number of bytes transferred.


NEXT UP previous
Next: Calling C Functions