Features


Multi-Threaded C Functions

AJM Beddow


Mr. Beddow is an electrical engineer with 15 years experience in electronic and computer system design. His current project involves real-time distributed databases. You may contact him at P.O. Box 36 Wantage, Oxon, OX12 8LL, United Kingdom.

OS/2 incorporates lightweight threads to implement parallel control with efficient use of system resources. You can construct functions that create the context for multi-threaded execution, control the execution of the threads, and remove the multi-thread context when the thread functions terminate. You can do this with the standard C library under any operating system.

The example of multi-threading uses an explicit function call to change thread context to the next thread of execution. This switch is normally made when an I/O resource is blocked and a thread is required to wait on the resource. The disadvantage of this non-preemptive thread switching is that you must add explicit thread switches within a thread's computation intensive parts to prevent its hogging the CPU. The advantage of non-preemptive thread switching is that reentrant calls to DOS can't run. The result is generally more efficient than the pre-emptive context switching for a non-reentrant operating system.

All thread functions are loaded as part of the one program. You create the threads by reserving stack space for each thread, creating a dispatch table, and dispatching the first thread. If all threads terminate, then the stack context space is recovered and single thread execution continues where it left off. The thread creation function is non-reentrant. It cannot be called from inside a spawned thread.

How It Is Done

To accomplish multi-threading in C, use the standard functions longjmp() and setjmp(). The setjmp() function takes an environment pointer and creates an environment for the current machine state, returning the value zero when it has done so. A longjmp() with the environment pointer as its parameter causes the program thread to return once more from a non-zero return value. The stack pointer and frame pointer are adjusted to the values saved in the environment when the setjmp() was originally called. This is the basis of the mechanism for switching thread context.

Listing 1 is an example of setjmp() and longjmp() functions for small model C programs. To return to a thread with its stack context intact, you must reserve a stack area for each thread. You achieve this by recursively calling a thread creation function with a large automatic array, and modifying the stack pointer in a thread environment to point to the top of the array.

Listing 2 shows my multi-threading functions. The function thread() takes a null terminated list of function pointers and copies the pointers into the threaddata structure array. Thread() then sets up a root environment and calls threadrun(). Threadrun() calls itself recursively creating an environment for each thread on the stack and storing a pointer to the environment in the threaddata structure array. It also modifies each stack pointer in the environment to point to the top of its automatic array stack-space.

Figure 1 shows the resulting stack arrangement of individual thread environments and stacks. Threadrun() then makes a longjmp() call to the first thread and returns to the setjmp point in threadrun() with a return value of -1. The first thread function pointer is then dispatched and the first thread runs. You make subsequent thread switches by calling threadswitch(), which sets the thread state flag to waiting, saves the thread environment, and calls longjmp() with the next thread environment to dispatch the next thread. When a function terminates, it returns to threadrun() which sets the thread state to terminated and calls threadswitch() to run the next available thread. When threadrun() finds all threads have terminated, it calls longjmp() with a pointer to the root environment, the thread stack spaces are recovered, and single (normal) thread execution resumes.

Note that threadswitch() calls have no effect in single thread execution and can be embedded into functions with no ill effects to single thread execution. The macro SPINDX defines the offset in words of the stack pointer parameter in the environment space. Use DEBUG on a setjmp() call to determine the offset for your own C compiler.

Example And Uses

In Listing 3, the main() function calls thread() with a list of three function pointers. Each function has two printf() function calls with a thread switch between them. Each function executes in turn to print one statement at a time. Aside from this example, multi-threading allows execution sharing between program function components to be separated from the function control flow. A state-sequencing function for example, may process multiple character streams using a state-sequencer thread for each stream and thread switching when a input stream is blocked. In real- time applications this is a low overhead method of resource sharing between non-time critical tasks.