--------------------------------------------------------------------------------
__ _____ __ _ / /___ _ / __(_)___ ____ ____/ /___ _____ (_) / / __ `/ / /_/ / __ \/_ / / __ / __ `/ __ \/ / / / /_/ / / __/ / / / / / /_/ /_/ / /_/ / / / / / /_/\__,_/ /_/ /_/_/ /_/ /___/\__,_/\__,_/_/ /_/_/
-------------------------------------------------------------------------------- | blog | blog_archive | about | contact | tutoring | --------------------------------------------------------------------------------
Here's a short tutorial on what threads are and why they're important. This is inspired by a question I got on Discord, I figured that I might as well write up a long-form response as it might help other people. Hopefully there'll be more blog posts incoming in the near future :)
Early personal computers were only able to run a single program at a time. This affected early operating systems like MS-DOS--whenever you run a program in MS-DOS it gets sole control of your CPU. This works perfectly fine when the only way to use the computer is through a command-line interface--you'll type a program's name, it'll run for a bit, and eventually it'll exit and once again MS-DOS will be in control of your CPU.
However, this is kind of limiting. You can't edit a text file and listen to your collection of MIDI music files at the same time. You can't run multiple different command-line sessions. And worst of all, if you have a graphical interface to your computer, you can't have multiple windows open at the same time.
Single-task operating systems had some severe limitations. Pretty much none of these operating systems are around today: most modern systems are multitasking.
Multitasking operating systems have the ability to run multiple programs at the same time. Not only does this now let you run programs in the background of your computer, it now means that you can have your browser open at the same time as your IDE. It's probably hard to imagine a computer that doesn't let you run multiple programs.
To accomplish this your operating system has to do a bit of work, your processor can still only run a single program at one time. To give the illusion of multiple programs running simultaneously the operating system has to rapidly switch between all currently running programs, called threads, letting each run for a fraction of a second at a time. This process is called context switching, specifically thread context switching. However, the operating system has to figure out exactly when to switch threads. There's two major ways to do this, through cooperative multitasking and preemptive multitasking.
Cooperative multitasking is named as such because threads are expected to cooperate with each other to make thread switches happen. Each thread can yield control to the operating system, letting the operating system know that it's ready for a thread switch to happen. Yielding typically happens through a system call provided by the operating system. Each thread is expected to yield withing a reasonable time, and this method will get great performance. However, as soon as you get a malicious process that refuses to yield your computer is frozen, running that malicious process until the end of time (or a hard reset).
Using preemptive multitasking threads will automatically be switched after a specified period of time, typically somewhere between 1ms and 100ms. The operating system will use a timer to generate periodic interrupts, stopping the current process and letting the operating system handle a switch into the next process. Typically the operating system will also let threads yield in addition to automatically preempting them. Preemptive multitasking fixes the issue of malicious program locking up the computer: now if a program tries to run forever without yielding it'll just get preempted by the operating system, letting other programs run like normal.
A thread of execution represents part of a running program. Some programs may run as a single thread, some may run using multiple threads. Many operating systems also make a distinction between threads and processes, but I won't here. To run, each thread needs its own set of registers, as well as its own virtual memory space. Each thread keeps track of its own instruction pointer, a pointer to the next assembly instruction to execute. Depending on the operating system some extra information might need to be stored with each thread (Wayless keeps track of a capability space for each thread). To switch between threads the operating system saves the values of the instruction pointer and every register. The operating system calls its scheduler, which figures out what thread to run next. Once the operating system knows what thread to run it loads the values of all the registers from the new thread, and switches from the old address space to the new space. Then the operating system sets the instruction pointer to that of the new thread. Now the computer is all set up to start executing the new thread, right where the thread left off.
The scheduler keeps track of all of the threads and decides what order to run them in. There's a lot of different scheduling algorithms, so I'll just cover a few here. One of the simplest ways to schedule threads is using a round robin scheduler. This scheduler uses a queue of threads, which can be implemented using a linked list. When the operating system switches threads it saves the current thread to the end of the queue. Then, the next thread to run is dequeued from the front of the queue. This ensures that every thread gets ran, and that threads aren't able to starve the system of CPU time. It also allows for a simple implementation. However, it doesn't allow for priority levels and potentially limits the responsiveness of high-priority threads.
A priority-based round robin scheduler adds priority levels. A different round-robin queue exists for each priority level. Processes from higher priority queues get ran first--each thread from the highest priority level is run, then each thread from the next-highest priority level, and so on. This scheduling algorithm extends the round robin scheduler, adding slightly to its complexity. Now programs that need to be highly responsive, like graphical programs, are guaranteed to run before lower priority programs.
Some computers have multiple cores, or multiple physical CPUs. A thread can run on every single core, potentially providing a huge increase in speed. As each program runs using at least one thread, this automatically provides a benefit when running many different programs simultaneously. However, breaking your program up into multiple threads usually takes a bit of work (modulo programming language). If you can break your program up into multiple threads then each of those threads can run in parallel, given enough cores, giving a massive boost to performance. If your program is only a single thread then it won't be able to take advantage of the multiple cores.