This was a real-time data-acquisition process for a medical application that took ECG data transmitted over the phone line and returned an automatically-generated English-language diagnosis of that ECG to the hospital within ten minutes.

The incoming A/D data from three channels per phone line for ten or twenty phone lines came in every 2 milliseconds. One level of interrupt was assigned to that device, and a task was attached to that interrupt. The job of that was to store each sample in the appropriate buffer properly multiplexed. When the buffer was full, this task would trigger a software interrupt for the routine writing the buffers to disk. This was its own task as well.

In addition to the analog data coming in, there were also touch-tone style tones coming in that a) gave patient identification numbers b) signalled the end of groups of data (data was transmitted in four groups of three leads), and signalled the end of the phone call. This had its own interrupt task as well.

Similarly, when data was written to disk, it was simultaneously written to tape. That had its own task as well. When the phone call was complete, it was put in a queue, and fed to the analysis system. Reading the data and properly assembling it was another interrupt driven task. Then there was either punching paper tape and later sending data out over phone lines, and that had its set of tasks as well.

Interrupts were used because that is pretty much how you did things in that environment. There is a lot of stuff going on all at the same time, and nobody better be busy-waiting anything. It all needed to be overlapped.

We eventually figured out that these tasks were often pairwise synchronous tasks that used interrupts to "hand over" control to its cooperating partner. So the idea of a coroutine was implemented to make the code clearer. You could think of the main loop of each of these tasks as a loop. When it came to the point of needing input, we developed a convention that essentially translated to "get me an interrupt" and the current IP and registers would be saved in the tasks context area, and the task suspended. Then, when it woke up, you would be at the next instruction following the "get me an interrupt" line. With this technique, the system that made the phone calls and sent the messages (tasks for dialing, sending characters, reading from disk, managing tape logging) was developed with only one single thread error and no multi thread errors. I helped with the design of that part but not the coding.

So for hard real time, you kind of need to deal with the interrupts.

Not sure what you mean by How did you manage the much-lower-levelness of the language. At that time, the alternative was Fortran, so we were before C, bliss, and the like. There just wasn't an alternative.

Yes, the reasons that coroutines work in assembler (See Knuth's description) is that you can save the state of the execution path and start right back up where you left off.

Well I think your proposal has merit, but the control you mention is also done in languages like Python, C#, Ruby with the 'yield' statement.

Lisp to bare metal is good, but stuff today is so much faster that it changes the equation entirely. We were doing this on a machine with 3.5 microsecond instruction times (at best) and the whole thing resided in a machine with 36k of 32-bit words. So small was important too, although that seemed big at the time.

So the question is how much control do you really need with machines today that will give you a metric truckload of instructions in a microsecond and you have to pay more money to get memory smaller than a gig? Heck I even get microseconds with SBCL.

Smaller language is good; but the selection of features is kind of key, cause you can give yourself a combinatorial rash if you aren't careful. Not sure what language I would choose these days--likely to start with something you could express the problem in nicely, and see if the timing works out. I learned a rule then which is "first make it right, then make it fast".