Thus far, we have seen the development of the basic abstractions that the OS performs. We have
seen how to take a single physical CPU and turn it into multiple virtual CPUs, thus enabling the
illusion of multiple programs running at the same time. We have also seen how to create the
illusion of a large, private virtual memory for each process; this abstraction of the address space
enables each program to behave as if it has its own memory when indeed the OS is secretly
multiplexing address spaces across physical memory (and sometimes, disk).
In this note, we introduce a new abstraction for a single running process: that of a thread.
Instead of our classic view of a single point of execution within a program (i.e., a single PC
where instructions are being fetched from and executed), a multi-threaded program has more
than one point of execution (i.e., multiple PCs, each of which is being fetched and executed
from). Perhaps another way to think of this is that each thread is very much like a separate
process, except for one difference: they share the same address space and thus can access
the same data.
The state of a single thread is thus very similar to that of a process. If has a program counter
(PC) that tracks where the private set of registers it uses for computation; thus, if there are two
threads of that are running on a single processor, when switching from running one (T1) to
running the other (T2), a context switch must take place. The context swtich between threads
is quite similar to the context switch between processes, as the register state of T1 must be
saved and the register state of T2 restored before running T2. With processes, we saved state
to a process control block; now, we will need one or more thread control blocks to store the
state of each thread of a process. There is one major difference, though, in the context switch
we perform between threads as compared to processes: the address space remains the same
(i.e., there is no need to switch which page table we are using).
One other major difference between threads and processes concerns the stack. In our simple
model of the address space of a classic process (which we can now call a single-threaded
process), there is a single stack, usually residing at the bottom of the address space.
However, in a multi-threaded process, each thread runs independently and of course may
call into various routines to do whatever work it is doing. Instead of a single stack in a
space, there will be one per thread. Let's say we have a multithreaded process that has
two threads in it; the resulting address space looks different.
In this figure, you can see two stacks spread throughout the address space of the process.
Thus, any stack-allocated variables, parameters, return values, and othre things that we
put on stack will be placed in what is sometimes called thread-local storage, i.e., the stack
of the relevant thread.
You might also notice how this ruins our beautiful address space layout. Before, the stack
and heap could grow independently and trouble only arose when you ran out of room in
the address space. Here, we no longer have such a nice situation. Fortunately, this is
usually OK, as stacks do not generally have to be very large (the exception being in
programs that make heavy use of recurison).