A thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system, although implementation of threads and processes differs between operating systems.
The multiple threads of a given process may be executed concurrently (via multithreading capabilities), sharing resources such as memory, while different processes do not share these resources. In particular, the threads of a process share its executable code and the values of its dynamically allocated variables and non-thread-local global variables at any given time.
Tip
Threads made an early appearance under the name of “tasks” in IBM’s batch processing operating system, OS/360, in 1967 but became more common in early 200s as CPU began to have multiple cors.
Tip
The term “thread” is credited to Victor A. Vyssotsky.
Processes
A process contains one or more kernel threads. It is a unit of resources while a thread is a unit of scheduling and execution. rocesses are isolated by process isolation, and do not share address spaces or file resources except through explicit methods such as inheriting file handles or shared memory segments, or mapping the same file in a shared way – see Interprocess Communication
A process is a “heavyweight” unit of kernel scheduling, as creating, destroying, and switching processes is relatively expensive.Processes are typically preemptively multitasked, and process switching is relatively expensive, beyond basic cost of context switching, due to issues such as cache flushing (in particular, process switching changes virtual memory addressing, causing invalidation and thus flushing of an untagged translation lookaside buffer (TLB), notably on x86).
Kernel Threads
A kernel thread is a “lightweight” unit of kernel scheduling and at least one kernel thread exists within each process. Kernel threads are preemptively multitasked if the operating system’s process scheduler is preemptive.
Kernel threads do not own resources except for a stack, a copy of the registers including the program counter, and thread-local storage (if any), and are thus relatively cheap to create and destroy. Since threads share the same address space, Translation Lookaside Buffer remains valid after switching between threads in the same process
User Threads
Threads are sometimes implemented in userspace libraries, thus called user threads. The kernel is unaware of them, so they are managed and scheduled in userspace. Some implementations base their user threads on top of several kernel threads, to benefit from multi-processor machines (M:N model). User threads as implemented by virtual machines are also called green threads.
- Because user-threads are in userspace, context switching doesn’t require the kernel at all, and is very efficient
- Since scheduling occurs in userspace, the policy can be tailored to the program need
- User threads are not managed by the OS scheduler, and a blocking call will prevent other user threads from running
Blocking in user-threads
When an I/O operation is initiated, a system call is made, and does not return until the I/O operation has been completed. In the intervening period, the entire process is “blocked” by the kernel and cannot run, which starves other user threads and fibers in the same process from executing. A common solution to this problem (used by many green threads implementations) is providing an I/O API that implements an interface that blocks the calling thread, rather than the entire process, by using non-blocking I/O internally, and scheduling another user thread or fiber while the I/O operation is in progress.
Tip
Some runtimes like the Go runtime automatically handle this problem by using a separate thread for I/O
Fibers
Fibers are an even lighter unit of scheduling which are cooperatively scheduled: a running fiber must explicitly “yield” to allow another fiber to run, which makes their implementation much easier than kernel or user threads. A fiber can be scheduled to run in any thread, allowing applications to gain performance by managing scheduling themselves, instead of relying on the kernel scheduler (which may not be tuned for the application).
Tip
Coroutines have become increasingly popular because they integrate seamlessly with modern language constructs like async/await. Coroutines offer language-level abstractions that make managing asynchronous workflows easier, whereas fibers are system-level constructs that require explicit scheduling by the developer.
Threads vs processes
Threads differ from traditional multitasking operating-system processes in several ways:
- processes are typically independent, while threads exist as subsets of a process
- processes carry considerably more state information than threads, whereas multiple threads within a process share process state as well as memory and other resources
- processes have separate address spaces, whereas threads share their address space
- processes interact only through system-provided inter-process communication mechanisms
- context switching between threads in the same process typically occurs faster than context switching between processes Systems such as Windows NT and OS/2 are said to have cheap threads and expensive processes; in other operating systems there is not so great a difference except in the cost of an address-space switch, which on some architectures (notably x86) results in a Translation Lookaside Buffer flush.
Note
Thread crashes a process: due to threads sharing the same address space, an illegal operation performed by a thread can crash the entire process; therefore, one misbehaving thread can disrupt the processing of all the other threads in the application.
Scheduling
Operating systems schedule threads either preemptively or cooperatively. Multi-user operating systems generally favor preemptive multithreading for its finer-grained control over execution time via context switching. However, preemptive scheduling may context-switch threads at moments unanticipated by programmers, thus causing lock convoy, priority inversion, or other side-effects.
Tip
Priority inheritance is a mitigation technique for priority inversion. It is a technique that allows a lower-priority thread temporarily inheriting the priority of an higher-priority thread it is blocking
In contrast, cooperative multi-threading relies on threads to relinquish control of execution, thus ensuring that threads run to completion. This can cause problems if a cooperatively multitasked thread blocks by waiting on a resource or if it starves other threads by not yielding control of execution during intensive computation.
Note
Until early 2000 computers were both single-core and no support for hardware threads so multi-threading was chiaved by time slicing. Still, because full process context-switches were expensive, threads were used even on single-core single threaded systems
Threading Models
Depending on how the user-created threads map to schedulable entities in the Kernel, we have different threading models
1:1 (kernel-level threading)
Threads created by the user in a 1:1 correspondence with schedulable entities in the kernel are the simplest possible threading implementation. OS/2 and Win32 used this approach from the start, while on Linux the GNU C Library implements this approach (via the NPTL or older LinuxThreads). This approach is also used by Solaris, NetBSD, FreeBSD, macOS, and iOS.
M:1 (user-level threading)
An M:1 model implies that all application-level threads map to one kernel-level scheduled entity and the kernel has no knowledge of the application threads. Context switching can be done very quickly and it can be implemented even on simple kernels which do not support threading. However, is that it cannot benefit from the hardware acceleration on multithreaded processors or multi-processor computers: there is never more than one thread being scheduled at the same time.
Tip
The GNU Portable Threads uses User-level threading, as does State Threads.
M:N (hybrid threading)
M:N maps M user threads into N kernel entities and the threading library is responsible for scheduling user threads on the available schedulable entities; this makes context switching of threads very fast, as it avoids system calls, but the threading library more complex.
Tip
At OS level, M:N has largely fallen out of favor while it is implemented at application library level, using frameworks like Tokio, Zio, or by the Go Runtime. It is much more feasible to have an M:N solution that works in a specific context rather than a general purpose one working across all the processes and threads
Examples of hybrid threading implementations are:
- Scheduler activations used by older versions of the NetBSD native POSIX threads library implementation (an M:N model as opposed to a 1:1 kernel or userspace implementation model)
- Light-weight processes used by older versions of the Solaris operating system
- Marcel from the PM2 project.
- The OS for the Tera-Cray MTA-2
- The Glasgow Haskell Compiler (GHC) for the language Haskell uses lightweight threads which are scheduled on operating system threads.
History of threading models in Unix systems
- SunOS 4.x (introduced in 1989) implemented lightweight processes (LWPs). These were among the first implementations of kernel threads in Unix-like systems.
- NetBSD 2.x+ (released in 2004) and DragonFly BSD (forked in 2003) implemented LWPs as kernel threads (1:1 model).
- SunOS 5.2 through SunOS 5.8 (spanning 1993 to 2000) implemented a two-level model, multiplexing one or more user-level threads on each kernel thread (M:N model).
- SunOS 5.9 (released in 2002) and later versions, as well as NetBSD 5 (released in 2009), eliminated user-thread support, returning to a 1:1 model.
- FreeBSD 5 (released in 2003) implemented the M:N model. FreeBSD 6 (released in 2005) supported both 1:1 and M:N threading, allowing users to configure which to use with /etc/libmap.conf.
- Starting with FreeBSD 7 (released in 2008), the 1:1 model became the default threading model.
- FreeBSD 8 (released in 2009) no longer supports the M:N model, fully transitioning to 1:1 threading.