The Classical Thread Model

The Classical Thread Model

Now that we have seen why threads might be helpful and how they can be used, let us examine the idea a bit more closely. The process model is based on two independent ideas: resource grouping and execution. Often it is beneficial to separate them; this is where threads come in. First we will consider the classical thread model; after that we will look at the Linux thread model, which blurs the line between processes and threads,

One way of looking at a process is that it is a way to group related resources together. A process has an address space containing program text and data, as well as other resources. These resource may contain open files, child processes, pending alarms, signal handlers, accounting information, and more. By putting them together in the form of a process, they can be handled more easily.

The other idea a process has is a thread of execution, generally shortened to just thread. The thread has a program counter that keeps track of which instruction to execute next. It has registers, which hold its current working variables. It has a stack, which contains the execution history, with one frame for each procedure called but not yet returned from. Though a thread must execute in some process, the thread and its process are different ideas and can be treated independently. Processes are used to group resources together; threads are the entities scheduled for execution on the CPU.

What threads add to the process model is to allow multiple executions to take place in the same process environment, to a large degree independent of one another. Having multiple threads running in parallel in one process is similar to having multiple processes running in parallel in one computer. In the former case, the threads share an address space and other resources. In the latter case, processes share physical memory, disks, printers, and other resources. Because threads have some of the properties of processes, they are sometimes called lightweight processes. The term multithreading is also used to explain the situation of allowing various threads in the same process.  As we saw in "INTRODUCTION", some CPUs have direct hardware support for multithreading and allow thread switches to happen on a nanosecond time scale.

In Fig.1.(a) we see three usual processes. Each process has its own address space and a single thread of control. On the contrary, in Fig.1.(b) we see a single process with three threads of control. Though in both cases we have three threads, in Fig.1.(a) each of them operates in a different address space, whereas in Fig.1.(b) all three of them share the same address space.

Three processes each with one thread

When a multithreaded process is run on a single-CPU system, the threads take turns running. In Fig.1 of "The Process Model", we saw how multiprogramming of processes works. By switching back and forth among various processes, the system gives the false impression of separate sequential processes running in parallel. Multithreading works the same way. The CPU switches quickly back and forth among the threads, providing the false impression that the threads are running in parallel, although on a slower CPU than the real one. With three compute-bound threads in a process, the threads would appear to be running in parallel, each one on a CPU with one-third the speed of the real CPU.

Different threads in a process are not as independent as different processes. All threads have exactly the same address space, which means that they also share the same global variables. Since every thread can access every memory address within the process address space, one thread can read, write, or even wipe out another thread's stack. There is no protection between threads because (1) it is impossible, and (2) it should not be necessary. Unlike different processes, which may be from different users and which may be hostile to one another, a process is always owned by a single user, who has most likely created various threads so that they can cooperate, not fight. In addition to sharing an address space, all the threads can share the same set of open files, child processes, alarms, and signals, and so on, as shown in Fig. 2. Therefore the organization of Fig.1.(a) would be used when the three processes are basically unrelated, whereas Fig.1.(b) would be suitable when the three threads are really part of the same job and are actively and closely cooperating with each other.

The first column lists some items shared by all threads in a process

The items in the first column are process properties, not thread properties. For instance, if one thread opens a file, that file is visible to the other threads in the process and they can read and write it. This is logical, since the process is the unit of resource management, not the thread. If each thread had its own address space, open files, pending alarms, and so on, it would be a separate process. What we are trying to achieve with the thread concept is the ability for various threads of execution to share a set of resources so that they can work together closely to carry out some task.

Like a traditional process (i.e., a process with only one thread), a thread can be in any one of various states: running, blocked, ready, or terminated. A running thread currently has the CPU and is active. A blocked thread is waiting for some event to unblock it. For example, when a thread performs a system call to read from the keyboard, it is blocked until input is typed. A thread can block waiting for some external event to happen or for some other thread to unblock it. A ready thread is scheduled to run and will as soon as its turn comes up.  The transitions between thread states are the same as the transitions between process states and are illustrated in "Process States" figure.
It is important to realize that each thread has its own stack, as demonstrated in Fig. 3. Each thread's stack includes one frame for each procedure called but not yet returned from. This frame contains the procedure's local variables and the return address to use when the procedure call has finished. For instance, if procedure X calls procedure Y and Y calls procedure Z, then while Z is executing, the frames for X, Y, and Z will all be on the stack. Each thread will normally call different procedures and thus have a different execution history. This is why each thread needs its own stack.

Each thread has its own stack

When multithreading is present, processes usually start with a single thread present. This thread has the ability to produce new threads by calling a library procedure, for instance, thread_create . A parameter to thread_create typically specifies the name of a procedure for the new thread to run. It is not required (or even possible) to specify anything about the new thread's address space, since it automatically runs in the address space of the creating thread. Sometimes threads are hierarchical, with a parent-child relationship, but often no such relationship exists, with all threads being equal. With or without a hierarchical relationship, the creating thread is generally returned a thread identifier that names the new thread.

When a thread has finished its work, it can exit by calling a library procedure, say, thread_exit. It then disappears and is no longer schedulable. In some thread systems, one thread can wait for a (particular) thread to exit by calling a procedure, for instance, thread_join.  This procedure blocks the calling thread until a (particular) thread has exited. In this regard, thread creation and termination is very much
like process creation and termination, with approximately the same options as well.

Another general thread call is thread_yield, which allows a thread to willingly give up the CPU to let another thread run. Such a call is important because there is no clock interrupt to actually enforce multiprogramming as there is with processes. Therefore it is important for threads to be polite and willingly surrender the CPU from time to time to give other threads a chance to run. Other calls allow one thread to wait for another thread to finish some work, for a thread to announce that it has finished some work, and so on.

While threads are sometimes helpful, they also introduce a number of problems into the programming model. To start with, consider the effects of the UNIX fork system call. If the parent process has various threads, should the child also have them? If not, the process may not function properly, since all of them may be important.

However, if the child process gets as many threads as the parent, what happens if a thread in the parent was blocked on a read call, say,  from the keyboard? Are two threads now blocked on the keyboard, one in the parent and one in the child? When a line is typed, do both threads get a copy of it? Only the parent? Only the child? The same problem exists with open network connections.

Another class of problems is related to the fact that threads share many data structures. What happens if one thread closes a file while another one is still reading from it? Consider that one thread notices that there is too little memory and starts allocating more memory.  Partway through, a thread switch takes place, and the new thread also notices that there is too little memory and also starts allocating more memory. Memory will probably be allocated twice. These problems can be solved with some effort, but careful thought and design are required to make multithreaded programs work properly.


system call, threads, address space