On Hacker News there’s an article entitled a Brief History of Threads, but it really isn’t. I thought I’d write my own history.
What is a “thread”?
What we are talking about are kernel threads. A single process has one or more threads of execution that all share the same process resources, like file handles and memory space, but they have their own unique stack and program counter (the thing that points to the code being executed). These threads are scheduled separately by the operating-system with preemptive multitasking and running on multiple CPUs.
In the old days, a process had only one thread. If you needed to do something that required multiple threads, you created multiple processes. This was resource intensive.
We are not talking about userspace threads. I need to discuss this first because there is a lot of confusion about this. For example, the original article claims that threads first appeared back on IBM S/360 mainframes in the 1960s. That’s not true. Userspace threads appeared there, not kernel threads.
A userspace thread where you create additional stacks in userspace and use cooperative multitasking to switch between them. You can do this in basic C code by using malloc() to allocate memory for another stack, and setjump() and longjump() to swap stacks. You call some sort of yield function to cede control back to a scheduler, which swaps stacks and continues executing another userspace thread. This yield function is often hidden behind APIs, so you call functions without really paying attention to the fact that it might yield control.
Such userspace threads are damn useful. For example, the Java runtime supports “green threads” that does this for you underneath. On network programming, when waiting to receive incoming data, your thread needs to stop and wait. In the Java runtime, it can transparently switch userspace threads without interacting with the kernel. You get most the benefits of kernel threads, but greater efficiency in userspace.
Likewise, the Go language “goroutines” are essentially userspace threads. People love the Go programming language because of the ease in creating scalable network services.
Userspace threads are cooperative multitasking. If they sit in an infinite loop without either yielding control, or calling a function that transparently yields control, then no other userspace thread will run. This can be bad — writing code for userspace threads means writing code differently.
Kernel threads are very different. They use preemptive multitasking, for one thing, so they can interrupt infinite loops.
In the past, before kernel threads, the kernel would maintain a list of all running processes. A timer chip would provide an occasional interrupt, like every 10 milliseconds. This would save the state of the current process, then transfer control to the kernel. The kernel would look through the list and find some other runnable process, then transfer control to that process. In that way, a single CPU could appear to run multiple processes at the same time.
Kernel threads is the same concept, except now a process can have multiple threads of execution. The scheduler now uses a table of all threads to choose from instead of all processes.
Some early implementations simply used the same process table, but allowed processes that would share address space and resources, making them effectively the same thing as threads. A process with two threads would then appear as two entries in the process table.
The point is that kernel threads are different than userspace threads. Userspace threads have always existed, kernel threads are a relatively recent invention.
We normally discuss threads as being a lighter weight alternative to forking a full process, but we should also see them has a heavier weight alternative to userspace space threads.
There are two histories here.
One is the history of full operating systems, like Unix, that had memory protection and preemptive multitasking. They had heavy weight processes and progressed to adding threads.
The other is the history of toy operating systems (from the modern perspective) that only had cooperative multitasking with userspace threads, like Windows 3.0, and Macintosh prior to MacOS X. These eventually grew up to full operating systems
OS/2 (1987)
The first mainstream operating-system with threads was OS/2 released in 1987 for the Intel 80286 processor.
This operating-system is largely forgotten by history, but was a seminal development at the time. Modern Windows is based upon Windows NT, which is in turn based upon OS/2.
Around 1987, the world was dominated by MS-DOS for PC-compatible desktop computers. This was a toy operating-system. There were versions of Unix available for the PC, like Microsoft’s own Xenix, but they were unsatisfactory for a number of reasons, the most important of which is that it couldn’t run MS-DOS programs (backwards compatibility).
IBM paid Microsoft to develop OS/2 — a full operating system with backwards compatibility with MS-DOS.
OS/2 had all the features of a full operating system, meaning memory protection and preemptive multitasking. It ran multiple copies of MS-DOS simultaneously, though only one could be “full screen” at a time.
There are lots to be said about this, but the thing to remember is that the first 1.0 release of OS/2 had threads as we know them today.
I’m not sure why OS/2 had threads. The main justification for other operating systems was multi-CPU support, because a programmer would want to split one process across multiple CPUs, and each CPU would need their own thread.
Mach and NeXT (1989)
The “Mach” kernel was a project from 1985 with a bunch of innovations. Among its features were multi-CPU support, and hence, threads.
It was first released as part of a commercial operating-system in 1989 with Steve Jobs’s “NeXT” computer. If you’ll remember, Steve Jobs had been fired from Apple in the 1980s, so he created a competitor company, NeXT. It was based upon the same CPU as the Macintosh, but with a Unix operating system. That operating system’s kernel was based upon Mach, with a BSD shell around it.
In 1996, Apple purchased NeXT to be the basis for the next generation Macintosh, where the operating-system was renamed MacOS X (today, “macOS”). Though MacOS X wasn’t released until 2001, conceptually, it’s had kernel threads support since 1989.
BeOS (1991)
Some other former Apple engineers split off to create an alternative, called BeOS. It also supported multiple CPUs and hence, multiple threads.
The thing that both NeXT and Be were chasing was the fact that Apple was failing with its “Pink” operating system. Everyone knew that personal computers had to move from toy operating-systems (MacOS, MS-DOS, Windows) to a full operating-system (like Unix). But at the same time, big companies always struggle with a v2.0 rewrite of their successful project. Apple had started their “full operating-system” project in 1988 called “Pink”, and it was already flailing in 1990.
Engineers knew they could split off and form a small team that could beat Apple to the goal, even though Apple already had 2 years of development behind them. They knew that Apple would eventually give up and buy a competitor.
Which is exactly what Apple did, though they chose NeXT over Be, making BeOS a failure. But engineers still consider it an elegant system.
The signature feature of BeOS was getting multi-media done right. Getting low-latency, reliably streaming audio from a computer is a surprisingly difficult job, which is why Linux still struggles at it. Since this was important to consumers, BeOS solved that first, and sorta built the rest of the system behind it. Threads and multi-CPU support were integral to this.
Solaris (1992)
Sun Microsystems was the most aggressively innovative Unix vendor around 1990. Their bombshell release of Solaris 2.0 in 1992 came with multi-CPU and threads, though they didn’t provide much support to developers until Solaris 2.2 in 1993.
It’s important to understand this in context of the Unix wars of 1988. All the vendors supported slightly incompatible versions, making it difficult to write software for one vendor’s Unix that would run on another.
Therefore, multiple standardization efforts were started.
One effort was POSIX, which simply standardized the APIs. This wasn’t highly regarded at the time, as people believed they needed to share the operating system code to be truly compatible.
One effort was “System V release 4” or “SVR4”, which combined the code from AT&Ts SRV3, BSD (including SunOS), and Microsoft Xenix into a single operating-system.
A third effort was “OSF/1”, based upon the Mach microkernel, in the same fashion as NeXT chose.
In terms of number of licenses, Xenix was the most important. It accounted for half of all Unix licenses. That’s because it ran on cheap PCs. But in terms of revenue, the biggest vendor was Sun. It was first to market with RISC computers in 1987 and was cleaning up. Both of these were in the SVR4 camp.
The rest of the vendors (not AT&T, Xenix, or Sun) joined the OSF/1 camp, including leaders like IBM, DEC, and HP.
Sun had released it’s first multi-CPU computers in 1991 using the older SunOS, but that kernel didn’t really support them well, having just a single kernel lock. In other words, the kernel was single CPU only, while userspace code could run on multiple CPUs. The hardware wasn’t really effective until Solaris 2 was released in 1992.
OSF/1 (1992)
DEC was really the only vendor that truly adopted OSF/1 after the Unix wars. The rest of the Unix vendors ended up simply extending their Unix variants with POSIX support.
While DEC’s first OSF/1 systems didn’t support multiple CPUs, they were still based upon the Mach kernel, and hence, supported threads.
Windows NT (1993)
After the wild success of Windows 3.0 in 1990, Microsoft decided to move away from OS/2, which wasn’t compatible with Windows. They instead created a new operating system from scratch with the intent of being Windows-compatible.
In 1993, Microsoft released Windows NT 3.0, a full operating system. The weird version number “3.0” was to drive home the point that consumers should see it as just another version of Windows. It looked and felt a lot like the Windows 3.0 they were accustomed to, while being a completely different thing — a full instead of toy operating-system.
The core operating system was based upon OS/2, DEC VMS, and some Unix features. It had backwards compatible sub-systems for MS-DOS, OS/2 1.3, and POSIX. Most importantly, it could run Windows 3.0 apps, inside what they called a “Windows on Window” subsystem or “WoW”.
WinNT inherited the concepts of kernel threads from OS/2, but more than that, Microsoft aggressively used threads throughout the operating system, and encouraged programmers to use them. It was also aggressively multi-CPU, so would likely have invented a version of threads even if it hadn’t inherited them from OS/2.
POSIX threads (1995)
The enduring legacy of the 1988 Unix wars is the POSIX standardization of the API. While all these companies intended to create a unified operating-system that each would license, they ended up just continuing to support their legacy product, adding POSIX compatibility as necessary.
When IEEE POSIX 1003.1c was standardized in 1995, some vendors already had kernel threads, some didn’t. For those who had kernel threads, they were often not quite compatible with the POSIX API.
For example, Linux’s first version of threads was in 1996, though it didn’t fully support the POSIX standard until 2003.
The BSD’s were even later than Linux. FreeBSD didn’t get it right until 2006.
M:N vs. 1:1 threads
As explained at the top, userspace threads are different than kernel threads.
A lot of early thread support tried to mix them behind a single API, called M:N threads. This meant there could be more of these userspace threads than kernel threads. That meant having to do both cooperative multitasking in userspace and preemptive multitasking in the kernel at the same time.
This was a broken idea, and everyone eventually gave up on it.
The straightforward kernel threads model you know today is called 1:1, meaning that as far as the kernel is concerned, for every kernel thread, there is only one userspace thread. You can still subdivide that userspace thread into multiple, cooperative multitasking userspace threads, but it something you do on top of an API like POSIX threads, not as part of it.
That’s why FreeBSD didn’t support POSIX threads until 2006, because they spent many years prior to that trying to get M:N working. This caused problems making the simpler 1:1 POSIX model work properly.
They failed to learn the lessons of history. Solaris had initially added M:N threading in 1992, and by 2002, gave it up, and went to the simpler 1:1 threading model. But in 2003, FreeBSD first added the M:N threading model, before they, too, gave it up in 2006.
Engineers love the “more powerful is better” idea of programming, when the reality is that “simpler is better”.
The upshot is that you shouldn’t even have to learn this M:N concept because it’s been abandoned. But yet, any discussion of the history of threads is going to talk about it.
Conclusion
The history of userspace threads goes all the way back. The toy operating-systems of the 1980s, like MacOS and Windows used them heavily with cooperative multitasking.
But for kernel threads, the history really goes back to OS/2 in 1987 and the first commercial Mach release (NeXT) in 1989.
Solaris had the reputation of the leading, most advanced operating-system of the 1990s, but it’s the heritage of Windows and macOS that did kernel threads first — and did them right.