It occurred to me recently that pretty much every project I’ve worked on in the last few years has been multi-threaded to some degree, and I thought that maybe it would be interesting to revisit the days before that became the standard feature it is today. The Atari ST is a good example of a machine that launched without much in the way of multitasking built-in, but which eventually evolved to include it.
The Atari ST was a great machine in many respects, but let’s be honest. It was never really on the cutting edge of technology. The Motorola 68000 processor it used had been out for nearly six years by the time the 520ST was first released, and the next-generation 68020 was already a year old. Even in a bygone age where new chips didn’t come out every year or so, the technology behind the 520ST was fairly mature already when the machine first launched.
The Evolution of the Atari ST
The ST used off-the-shelf components as much as possible, and this extended to the creation of the operating system software, TOS. At Digital Research, GEM (Graphics Environment Manager) was in the late stages of development for the PC when Atari approached them about using it for the ST. GEM was just a graphics library and GUI manager, not a complete operating system, so Atari still needed the core of an operating system. Digital Research had originally been founded around their CP/M operating system, and they had created a version for the 68000 called CP/M 68K. Atari grabbed that from DR as well and dubbed it “GEMDOS”.
For those in the audience who have never heard of some of this stuff, CP/M is the operating system that most computers based on the Intel 8080 and Zilog Z-80 processors used before Microsoft came out with MS-DOS. To simplify, it was more or less the same thing, namely a basic file system manager and text-based command line interface for running programs.
Ultimately, the main part of TOS that Atari did primarily by themselves was the low-level code that knew the important details about the hardware. This was divided into the “BIOS” which was essentially similar in structure to the BIOS on a PC, and the “eXtended BIOS” which was everything else.
One thing that wasn’t included in GEMDOS or the BIOS was any sort of support for preemptive multitasking. Looking back on it these days, this may seem like a huge, gaping hole in the design, but in those ancient times, preemptive multitasking was not yet a common feature for desktop computers. So while its absence may have been a disappointment to some, it was not the major catastrophe it would be today.
Why No Multitasking?
Why didn’t the ST have multitasking right out of the gate, and how did it eventually end up happening?
Back in the early 1980s, real multitasking was something you saw mainly on mainframe computers and sometimes on expensive Unix-based workstations. Desktop PC users could buy programs like IBM’s TopView or Quarterdeck’s DESCView which would give you the ability to load multiple programs into memory at the same time and switch back and forth between them. Not really multitasking so much as task-switching, but it was more than you could do with plain MSDOS. But these systems worked only with text-based programs, and compatibility problems were decidedly non-trivial, Not to mention how ridiculously easy it was to run out of memory. Many programs simply would not work under these environments. Furthermore, they only came out a short time before Microsoft released the first version of Windows, offering up a more standardized way to run programs side by side.
Part of the reason multitasking was not commonplace was that the hardware of the era was not really up to the task quite yet. The major issue was memory. Multitasking chews up memory pretty quickly, and before 1985 or so, desktop machines were generally limited to less than 1mb of RAM. To put that into perspective, these days your phone probably has at least 512mb of regular RAM, maybe more, and anywhere up to 64gb of flash RAM.
Even on big mainframe computers, memory was still an issue. The first problem was that memory was quite expensive back then, and secondly that any particular processor has a fixed limit on the range of memory addresses it can access on the system bus. A 16-bit processor like the Intel 8086 used in early PCs had a 20-bit address bus and thus could access up to 1024 kb (1mb) of RAM. Other processors had similar limits.
To get around these problems, the concept of virtual memory was created. This is where the system uses the hard drive, or other storage media, as a direct extension of RAM. Let’s say you want to run 5 programs which need 512kb each, but your computer only has 1mb of RAM installed. That’s where virtual memory comes in.
The virtual memory manager maintains a file on the hard drive, or other designated storage, called the “swap file”. When a program needs to access a block of RAM, the system checks to see if the program’s data needs to be loaded in from the swap file. If a memory swap is required, it writes out the current contents of that block of RAM to the swap file, then loads another portion of the swap file back into RAM. From the user’s perspective, this happens automatically with no additional action required. The only thing the user notices is a (hopefully) slight delay while the memory swap file is accessed.
In the early days, virtual memory was better than nothing, but it was often kind of a big clunky thing on desktop computers, with a very noticeable impact on performance. This was because they had to work around the fact that most early desktop machines lacked a sophisticated memory management unit (MMU).
An MMU sits on the system bus between the main CPU and RAM and monitors the CPU’s memory access. It divides RAM into blocks usually called “pages” and gives each one a process identifier that indicates which program owns the current contents. When a page is accessed by the CPU, the MMU compares its process identifier against the current process. If they match, the memory access occurs immediately. If they don’t, the MMU triggers an interrupt on the CPU which allows the operating system to do whatever virtual memory swapping is needed to load the correct data for the current program. This allows the virtual memory manager to work on relatively small chunks of memory at a time, minimizing the performance impact.
Memory management units had been used with virtual memory on mainframes and high-end workstations for many years, where multitasking and virtual memory was not only a standard feature but an absolute requirement, but they were rare on desktop computers because of the cost. Unfortunately, without an MMU, a virtual memory manager has no way to exert fine control over memory swapping. This means it has to swap each program’s entire memory space, or most of it, when switching between programs, even when that isn’t really necessary.
The relative inefficiency of the memory swapping process meant it wasn’t practical to switch between programs too often, or you’d be spending most of the computer’s processing power just swapping data in and out. This gave rise to a simplified flavor of multitasking called task switching, where the user would be able to load multiple programs but only one would really be active at a time. When the user wanted to switch to a different program, they would hit a particular keystroke or select a menu item, and the task switching manager would perform whatever virtual memory swap was needed to save the current state of the current program, and then load the previously saved state of the next program into memory. There would be a momentary delay when switching between programs, but it was still much faster and more convenient than having to quit one program and manually launch the other one.
Virtual memory on desktop systems started to become much more efficient and effective in the mid-1980s when more sophisticated memory management units were built into newer processors like the 80286 or 68030. Combined with the ability to address more RAM, this meant that desktop operating systems were finally able to move beyond simple task switching into real multitasking.
Cooperative Or Preemptive?
There are basically two forms of true multitasking: preemptive and cooperative. The main difference between these is how the operating system manages the process of switching back-and-forth between different tasks. Modern computing devices virtually always use preemptive multitasking. With this setup, the OS maintains a queue of all the execution threads which are currently running and periodically switches between them, halting the current one, saving its state, then restoring the previously saved state for the next thread and restarting it at the next instruction from where it left off on its previous turn.
A “thread” is basically one particular instance of code executing on the processor. Each thread belongs to a process, which is the collective name for how the OS keeps track of each program and the resources which belong to it. A process will always have at least one thread, but could easily have half a dozen or more. Depending on how many programs are loaded, there could easily be several dozen or even several hundred threads running at once.
In a typical modern operating system, which you launch a program, the main thread is known as the “UI thread” because it’s primarily responsible for managing the program’s user interface. When the program needs to perform some calculations or do some other sort of task that will take a significant amount of time, it creates what’s known as a “worker thread” to do it. The reason is so that the UI thread can continue to process UI events as needed. If you simply did the task in the UI thread, your user interface would be blocked until the task was completed, resulting in a program that is unresponsive.
In order to give each thread time to execute, the OS manages a system timer which periodically causes an interrupt. The interrupt service routine in the OS saves all the important information about the current thread, then passes control to the next thread waiting in the queue. This is known as context switching. It can also happen at other times, like when certain OS functions are called, or when a thread voluntarily gives the OS permission to switch early, rather than waiting for its allotted time slice to finish. Thus, each program has a chance to execute a little bit at a time. When you switch back and forth quickly enough, it looks to the user like all of the programs are executing at the same time. In fact, on a modern multi-core processor, they may be.
In the mid-1980s, early examples of multitasking on desktop systems used the cooperative model. This is because true preemptive multitasking has to be implemented at the very lowest level of the operating system, but early GUI environments like Microsoft Windows, GEM, and so forth were not complete operating systems. They were shells that relied on having MS-DOS or GEMDOS running on the system underneath for tasks like disk operations, launching programs, and so forth. Because those underlying operating systems did not support preemptive multitasking, the graphics shell had to use cooperative multitasking instead.
In a cooperative multitasking environment, the OS doesn’t interrupt programs to pass control to someone else. Instead, applications are expected to make periodic calls to the system’s event processing functions. The context switching between programs occurs at this time. The next application gets a chance to execute for awhile until it makes its own call to the event processing functions. Thus, as long as programs cooperate by making the appropriate API calls on a regular basis, things work well.
However, not everybody wants to, or is able to, play nice. And therein lies the problem. The biggest dilemma with a cooperative multitasking setup is is figuring out how to perform tasks which take a long time to finish. Programs cannot create a “worker” thread as we described earlier because just about everything runs in a single thread, including the operating system, the GUI’s window and menu handlers, other programs, etc.
The exception to the rule is interrupt-driven code. In a cooperative multitasking environment, it’s not uncommon for the system to have one or more interrupt routines, usually running off a timer, to help with at least a portion of the processing required to manage the user interface. This ensures that some of the most basic parts of the user interface are still responsive most of the time. However, there are limits to how much can be done this way. So if one program has to do some processor-intensive task, it’s still possible for the rest of the system to come to a halt in the meantime. Other programs don’t redraw their windows, menus can become unresponsive, etc.
The use of cooperative multitasking is reflected in the basic structure of how Windows applications work. When a program runs, initially all it really does is register a window class and create the window. Then it exits and sits idle, waiting for Windows to call the message handler routine. Basically, the life of a Windows application is waiting for and then responding to messages from Windows.
The Atari ST’s Approach
The lifecycle of a GEM application revolves around processing messages, much like a Windows application, but the details are different. With Windows, after an application has launched and registered its message handling routine, control returns to Windows, which thereafter calls the application only when there’s an event to be processed.
With GEM, when the program is launched, the application is complete control until it decides to ask the system if any events need to be processed. Instead of registering a callback function that gets all the various messages that need to be processed, a GEM application initializes itself and then enters a loop which uses a GEM AES function to query the system to see if any events have occurred that need to be processed.
If the program wants, this query can return control back to the application even if no events are in need of processing. Ideally the application should loop back into another query in most cases, but it doesn’t have to. A single application could have multiple versions of the event processing loop that it would use in different circumstances.
The Atari ST Startup Process (Before Multi-TOS)
When you power-up the Atari ST, it starts looking for programs in the AUTO folder of the boot disk. These are normally TSR (Terminate & Stay Resident) programs which serve to install device drivers or software services. Such programs usually have little or no interaction with the user beyond some simple text output. Once the AUTO folder is processed, it’s time to launch GEM AES (Application Environment Services). In addition to providing the API for windows, menus, event processing, etc., GEM AES was also responsible for opening the screen device using the GEM VDI graphics kernel.
One of the last things AES does at startup is to load any desktop accessory programs (using an “.ACC” extension) found in the root directory of the boot drive. Desk accessories are a special class of GEM application and in many ways are the basis for whatever claims may have been made for GEM being a multitasking system.
Once loaded by GEM AES, a desk accessory would initialize itself but in most cases would not open a window or do any other screen output. Instead of installing a menu bar, they would register a single menu item which would appear in the “Desk” menu of the current application’s menu bar. After loading, they would normal remain idle until they got a message indicating that their menu item had been selected, at which point they would wake up and open their main window or dialog box or whatever.
As each desk accessory finished initializing itself and called the event_multi function to poll for events, GEM AES would leave it in memory and load the next one.
After loading the available desk accessories, the AES would launch the system GUI shell, normally the GEM Desktop application, which was a relatively basic GUI-based file manager and program launcher. At that point, the user could browse files and launch programs as desired.
GEM Application Lifecycle
As with most GUI-based environments, a GEM application’s entire life revolved around processing the event loop. If you got an event with a message like WM_REDRAW or WM_TOPPED to indicate various window-related events, your program would be expected to respond by redrawing the window or taking whatever other action was indicated.
There were several different kinds of events: window & system messages, mouse events, keyboard events, but they mostly had one thing in common. They wanted your program to react to something that had been done by the user. The user had moved the mouse, or pressed a key, or maybe another window was closed and now yours was on top.
Compared to the previous generation of text-based programs, the new generation of programs for GEM, Windows, Macintosh, etc., required a decent-sized chunk of code just to open a window and get the basic event processing happening. In the early days there were no application frameworks like Microsoft’s MFC or WinForms, so it would take at least a few hundred lines of code just to get the ball rolling. And that’s before your program does any of the specialized tasks that make it more than a do-nothing demo.
As long as your program was doing the basic tasks of responding to messages, the cooperative nature of the multitasking worked reasonably well. But the problem, of course, is that programs have lots of things to do besides simply reacting to user-interface events. For example, a fax program may need to monitor a modem for incoming calls, or do some other sort of processing that takes awhile to be processed.
This is the situation where a modern program would simply create a worker thread which would operate independently of the user interface. It would do whatever processing was required and then somehow indicate to the user interface that it was completed.
But with GEM, we did not have that option. There were no worker threads. And as a practical matter, if you were writing a regular application, programmers were not too concerned about other programs becoming non-responsive for awhile when they were busy processing something. They simply let the system hang up as needed until they finished. It wasn’t ideal, but in those days most people generally viewed the current main application as owning the system, so they got away with it.
If you were writing a desk accessory, however, that wasn’t a good solution. Most desk accessories didn’t do anything too heavy-duty so it was never an issue. However, there were notable exceptions like fax software which ran as a desk accessory so it could receive faxes at any time. A desk accessory that regularly interfered with the operation of the main application would not be very popular for very long. Programmers had to figure out a way to make sure that event processing was not blocked while they were performing long tasks.
Most of the time, programmers simply stuck in a bunch of calls to the system event handler throughout their processing code. This worked well enough in some cases, but in others it was hard to predict how much time a particular chunk of code would take to execute, so the calls to the event handler were called at somewhat irregular intervals.
The problem was there wasn’t any one solution that would work for all applications. When I was doing developer support at Atari, this was a common topic among the questions I got from developers.
True Pre-Emptive Multitasking Comes To The ST
There were a few developers that would attempt to create a multitasking kernel for the ST, but the one that would finally gain some traction was Eric Smith’s MiNT (“MiNT Is Not TOS” at first, but later “MiNT Is Now TOS“), originally developed as shareware in the late 80’s while Smith was a university student.
The original goal of MiNT was to provide some extra functionality that was needed by a variety of GNU-based tools which were being ported to the Atari at that time, and to provide a true preemptive multitasking kernel. MiNT wrapped itself around GEMDOS and the BIOS and added functions for thread and process management. Atari’s GEMDOS guru, Allan Pratt, became familiar with MiNT and started looking at it as the underlying kernel for a new multitasking version of Atari’s operating system to be called MultiTOS. Pratt left Atari in 1992, but Eric Smith was soon hired and MultiTOS 1.0 was released soon afterwards.
Given that there was seven years worth of software on the market at that point, much of which had not been updated in awhile, MultiTOS did nothing short of an amazing job with regards to performance and compatibility. Not everything worked with it, but quite a surprising amount of programs worked more or less seamlessly.
The main problem with programs which didn’t work so well was the sharing of the screen with other applications. A lot of programs would use GEM VDI as a graphics library, but not use GEM’s window management or menus, so they were normally expecting to own the entire screen and not have to worry about desk accessories or other applications.
There may have been ways to sandbox such applications to make them co-exist more effectively, but we never got a chance to figure it out. Unfortunately, further development of MultiTOS and the Atari computer line more or less died shortly after the release of MultiTOS as all development personnel were moved to various projects for the Atari Jaguar game console.
Oh, how much easier things might have been if we’d had true multitasking from the start.
Other than the need for extra programming resources, there’s really no reason the Atari couldn’t have had multi-tasking from day 1. If you’re not doing virtual memory, it doesn’t take any special hardware to do preemptive multitasking, but it does require a great deal more complexity on the operating system side of things. Everything in the OS has to be capable of processing several simultaneous requests. There has to be a way to share system resources so that programs don’t step on each other. All of this takes significantly more memory and processing power than a cooperative multitasking environment, and both were still relatively expensive things back in the mid 80’s.