Making Single-Threaded Code Multithreaded

2.2.9 Making Single-Threaded Code Multithreaded

Many existing programs were written for single-threaded processes. Convert- ing these to multithreading is much trickier than it may at first appear. Below we will examine just a few of the pitfalls.

As a start, the code of a thread normally consists of multiple procedures, just like a process. These may have local variables, global variables, and parameters. Local variables and parameters do not cause any trouble, but variables that are glo- bal to a thread but not global to the entire program are a problem. These are vari- ables that are global in the sense that many procedures within the thread use them (as they might use any global variable), but other threads should logically leave them alone.

As an example, consider the errno variable maintained by UNIX. When a process (or a thread) makes a system call that fails, the error code is put into errno. In Fig. 2-19, thread 1 executes the system call access to find out if it has permis- sion to access a certain file. The operating system returns the answer in the global variable errno. After control has returned to thread 1, but before it has a chance to read errno, the scheduler decides that thread 1 has had enough CPU time for the moment and decides to switch to thread 2. Thread 2 executes an open call that fails, which causes errno to be overwritten and thread 1’s access code to be lost forever. When thread 1 starts up later, it will read the wrong value and behave incorrectly.

Thread 1

Thread 2

Access (errno set) T ime

Open (errno overwritten)

Errno inspected

Figure 2-19. Conflicts between threads over the use of a global variable.

Various solutions to this problem are possible. One is to prohibit global vari- ables altogether. Howev er worthy this ideal may be, it conflicts with much existing software. Another is to assign each thread its own private global variables, as shown in Fig. 2-20. In this way, each thread has its own private copy of errno and other global variables, so conflicts are avoided. In effect, this decision creates a

SEC. 2.2

THREADS

new scoping level, variables visible to all the procedures of a thread (but not to other threads), in addition to the existing scoping levels of variables visible only to one procedure and variables visible everywhere in the program.

Thread 1's code

Thread 2's code

Thread 1's stack

Thread 2's stack

Thread 1's globals

Thread 2's globals

Figure 2-20. Threads can have private global variables.

Accessing the private global variables is a bit tricky, howev er, since most pro- gramming languages have a way of expressing local variables and global variables, but not intermediate forms. It is possible to allocate a chunk of memory for the globals and pass it to each procedure in the thread as an extra parameter. While hardly an elegant solution, it works.

Alternatively, new library procedures can be introduced to create, set, and read these threadwide global variables. The first call might look like this:

create global("bufptr"); It allocates storage for a pointer called bufptr on the heap or in a special storage

area reserved for the calling thread. No matter where the storage is allocated, only the calling thread has access to the global variable. If another thread creates a glo- bal variable with the same name, it gets a different storage location that does not conflict with the existing one.

Tw o calls are needed to access global variables: one for writing them and the other for reading them. For writing, something like

set global("bufptr", &buf); will do. It stores the value of a pointer in the storage location previously created

by the call to create global. To read a global variable, the call might look like bufptr = read global("bufptr"); It returns the address stored in the global variable, so its data can be accessed.

CHAP. 2 The next problem in turning a single-threaded program into a multithreaded

PROCESSES AND THREADS

one is that many library procedures are not reentrant. That is, they were not de- signed to have a second call made to any giv en procedure while a previous call has not yet finished. For example, sending a message over the network may well be programmed to assemble the message in a fixed buffer within the library, then to trap to the kernel to send it. What happens if one thread has assembled its message in the buffer, then a clock interrupt forces a switch to a second thread that im- mediately overwrites the buffer with its own message?

Similarly, memory-allocation procedures such as malloc in UNIX, maintain crucial tables about memory usage, for example, a linked list of available chunks of memory. While malloc is busy updating these lists, they may temporarily be in an inconsistent state, with pointers that point nowhere. If a thread switch occurs while the tables are inconsistent and a new call comes in from a different thread, an invalid pointer may be used, leading to a program crash. Fixing all these problems effectively means rewriting the entire library. Doing so is a nontrivial activity with

a real possibility of introducing subtle errors.

A different solution is to provide each procedure with a jacket that sets a bit to mark the library as in use. Any attempt for another thread to use a library proce- dure while a previous call has not yet completed is blocked. Although this ap- proach can be made to work, it greatly eliminates potential parallelism.

Next, consider signals. Some signals are logically thread specific, whereas oth- ers are not. For example, if a thread calls alar m , it makes sense for the resulting signal to go to the thread that made the call. However, when threads are imple- mented entirely in user space, the kernel does not even know about threads and can hardly direct the signal to the right one. An additional complication occurs if a process may only have one alarm pending at a time and several threads call alar m independently.

Other signals, such as keyboard interrupt, are not thread specific. Who should catch them? One designated thread? All the threads? A newly created pop-up thread? Furthermore, what happens if one thread changes the signal handlers with- out telling other threads? And what happens if one thread wants to catch a particu- lar signal (say, the user hitting CTRL-C), and another thread wants this signal to terminate the process? This situation can arise if one or more threads run standard library procedures and others are user-written. Clearly, these wishes are incompati- ble. In general, signals are difficult enough to manage in a single-threaded envi- ronment. Going to a multithreaded environment does not make them any easier to handle.

One last problem introduced by threads is stack management. In many sys- tems, when a process’ stack overflows, the kernel just provides that process with more stack automatically. When a process has multiple threads, it must also have multiple stacks. If the kernel is not aware of all these stacks, it cannot grow them automatically upon stack fault. In fact, it may not even realize that a memory fault is related to the growth of some thread’s stack.

SEC. 2.2

THREADS

These problems are certainly not insurmountable, but they do show that just introducing threads into an existing system without a fairly substantial system redesign is not going to work at all. The semantics of system calls may have to be redefined and libraries rewritten, at the very least. And all of these things must be done in such a way as to remain backward compatible with existing programs for the limiting case of a process with only one thread. For additional information about threads, see Hauser et al. (1993), Marsh et al. (1991), and Rodrigues et al. (2010).