Introduction to Concurrency and Threads
This lesson introduces you to the subject of the upcoming chapters, concurrency, and threads.
We'll cover the following
Thus far, you have seen the development of the basic abstractions that the OS performs. You 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. You 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).
More than one point of execution
In this note, you will now be introduced to 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. It has a program counter (PC) that tracks where the program is fetching instructions from. Each thread has its own private set of registers it uses for computation; thus, if there are two threads 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 switch 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, you save state to a process control block (PCB); now, you’ll need one or more thread control blocks (TCBs) to store the state of each thread of a process. There is one major difference, though, in the context switch performed 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 you can now call a single-threaded process), there is a single stack, usually residing at the bottom of the address space (See left of the below figure).
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 the address space, there will be one per thread. Let’s say you have a multi-threaded process that has two threads in it; the resulting address space looks different (See right of the above figure).
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 other things that we put on the 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 the otherwise 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, you 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 recursion).
Get hands-on with 1400+ tech skills courses.