9. The Thread: A Virtual CPU#

If a process is a virtual computer, a thread is a virtual CPU. Modern computers have many CPUs, and many applications are written to exploit multiple virtual CPU either to enable progress to be made while some threads are blocked, or to enable the program to exploit multiple physical CPUs. We call such applications multi-threaded applications. All the threads are part of the same process, sharing memory, open files, etc… they only differ in the set of registers run on the CPU. Of course, one of those registers is the stack pointer, so each thread by default has its own stack.

One special case of a multi-threaded program is the kernel itself. While many applications may run on a single CPU, the kernel has to run on all the CPUs. The major challenge to write multi-threaded programs, of which the kernel is probably the most extreme example, is how to ensure that the threads that are running at the same time synchronize so that they don’t modify the same memory in incompatible ways at the same time. This is one of the most complex topics in operating systems, and we defer it to later (see synchronization) after we have finished discussing memory management and file systems.

9.1. Interface#

The standard interface for threads is posix threads. Like always, use man to find out more information about any of these calls. This interface is implemented in Linux using the pthread library on top of the system calls we have previously described. Core functions are:

  • err =  pthread_create(&id, attr, start_routine): creates a thread that will start executing at start_routine, returning status and the thread id

  • pthread_exit(retval): exit the current thread, returning to the parent the return value

  • id = pthread_self(): returns the id of the thread that called it

  • err = pthread_join(id, &retval): wait for thread specified by id to complete, and return its status.

In addition, there are a set of synchronization operations that we will discuss later.

9.2. Implementation#

If you want to exploit multiple CPUs the easiest way is to have the abstraction of threads implemented by the kernel, just like processes. You may have wondered why in Fig. 4.2 and Listing 8.1 Linux refers to the internal data structures as tasks rather than processes. The implementation of the thread abstraction in the Linux kernel is that each thread is its own task, with its own thread state, and all the threads in a process point to the same MM and file struct data structures. With such a design, everything we have talked about context switching and scheduling in the previous chapter directly applies to threads, and the fact that multiple of these threads are part of a single process is irrelevant.

You can also implement threads at user level. There are advantages to such implementation, where in user space one can quickly context switch between one thread and another without having to transition to the kernel and back. In fact, those of you taking EC440 at BU, you will be implementing your own user level threading system. If you implement threads totally at user level, then if any thread makes a system call, it will block all the other threads from running. Also, user-level threads if implemented on a single kernel level thread have no parallelism, i.e. they cannot exploit multiple CPUs.

There is also literature of research that multiplexes some number of user level threads on top of some number of kernel thread, with perhaps the best known work, Scheduler Activations [ABLL91], proposing specific support in a kernel to enable efficient design of user level threads.