1

I know the spectrum had the IM2 mode, but can I start multiple threads running simultaneously?

In example: can I play a song, while transfering large amounts of data, like in large ldi code blocks?

; start block copy 
ldi 
ldi
ldi
  • 3
    Please specify what kind or threading you ask about. There are many ways from cooperative all the way to hardware style threading. In general threading is not a hardware issue but OS/environment dependant, so specifying here what kind of 'startong' your question is about would be helpful. – Raffzahn Apr 6 at 23:30
  • Your question appears to address the more generic concept of "multitasking", which is possible even on simple 8-bit CPU's like the Z80. – Brian H Apr 7 at 16:00
  • 1
    You can multithread even without a computer. Play multiple simultaneous games of solitaire or chess for instance. Each deck of cards or board contains the state of the game, an "execution context". You just have to do some context switching when switching between games, and the switching takes some time, reducing your efficiency. For really parallel execution there has to be multiple persons, as happens in a two-person game of chess or card games. No need to task-switch if the number of running threads is equal to or smaller than the number of processors. – piiperi Reinstate Monica Apr 7 at 17:23
  • You can (and I have) implement multithreading on the MIX machine, which I suppose is somewhat more primitive than the Z80. You just stop running one thread and start running another thread. – another-dave Apr 7 at 17:49
8

Interrupt Mode 2 is not a ZX Spectrum feature, it's a feature of the Zilog Z80 CPU itself.

Per Raffzahn@'s comment, the answer to your question kinda depends on what kind of threading you're talking about.

There are generally two levels of abstraction when it comes to multithreading, and two meanings of the term. First is the hardware level Simultaneous Multi-Threading (SMT) - a feature of the superscalar CPUs that allows multiple instructions to be executed in parallel. Intel calls its implementation of this feature in their CPUs like Pentium 4 HT and Core i7 'Hyper-Threading'. Second is the software level multi-threading - a model of concurrent execution without true parallelism, but that makes it appear to a user that things are executed at the same time by quickly switching back and forth between multiple threads of execution. This is what all multitasking operating systems implement, even on CPUs that do support SMT and have multiple cores, because generally there are way more processes and threads that need to be executed than there are execution units, CPU cores, and CPU sockets in a computer system. On the systems with only scalar CPUs the second variant of multi-threading is the only available option.

Now to go back to Z80 and Spectrum - since Z80 is a scalar CPU, the first kind of multi-threading, the true simultaneous one, with parallel execution of the instructions, is not possible. But the second kind, where threads execute concurrently, but not at the same time in parallel, is definitely possible. You don't necessarily need memory protection for this, all you need for pre-emptive multitasking is interrupt mechanism (the implication being even without interrupt mechanism it should still be possible to implement cooperative multitasking like in Windows 1.x/2.x/3.x and classic macOS). In fact, there is an operating system called SymbOS that supports preemptive multitasking on machines like Amstrad CPC and MSX2 family. Those machines use Z80 CPU and lack memory protection just like ZX Spectrum. The main problem with running SymbOS on original ZX Spectrum is that the OS needs at least 128KiB RAM and currently doesn't support all of Spectrum's hardware. NOTE: the difference between threads and processes is in memory sharing, not scheduling and execution, so for this question I use them somewhat interchangeably.

The specifics of interrupt mechanism, registers, etc., mostly determine not whether it is possible to implement multitasking/multithreading, but how easy or difficult it is. E.g., the register set and memory model of Z80 are limited enough to make C compiler implementation more difficult, and the resulting compiled code to be much less efficient than hand-written assembly, but it has been done, so it's definitely not impossible to do so. In general, you can find plenty examples of programmers achieving many great things on a very limited hardware, it's more a question of persistence and ingenuity than possibility. And a cost/benefit tradeoff.

| improve this answer | |
  • 2
    Regarding your terminology, "multithreading" is a model of concurrent execution. It does not require (and often does not use) parallelism; i.e., the threads can alternate instead of running at the same time.. This is true even down to the hardware level: hyperthreading is non-parallel execution of two threads on a single core. I feel it would improve your answer if you carefully distinguished between concurrency and parallelism. – cjs Apr 7 at 3:58
  • The use of Z80 alternate register set can be used for context switching. As long as each thread uses a separate register set and only switches on context switch, the use of a NMI could be used to swap between the two threads. I dont know would be possible on a spectrum, it would depend on whether the firmware routines use the alternate registers. – Mark Williams Apr 7 at 11:58
  • @cjs Hyper-threading is just Intel's marketing name for its implementation of SMT - simultaneous multithreading. And as the name implies, it does allow more than one thread to be executed at the same time, taking advantage of the superscalar features of the processor. – moonwalker Apr 8 at 5:35
  • 1
    @MarkWilliams, using the alternate registers would be too limiting. You'd be stuck with just too tasks and couldn't use the alternate registers in task programs. The ZX Spectrum ROM makes extensive use of them. The existence of alternate registers doesn't automatically make them a good candidate for mt. – TonyM Apr 9 at 6:51
  • 1
    Relevant: a 1985 magazine article with a multitasking system for the spectrum -- and, incidentally, the first assembly language type-in program I ever typed up. – occipita May 21 at 20:33
7

All of the popular, early 8-bit CPU's support hardware and software interrupts. Therefore, they can all theoretically support preemptive multitasking, which I think is what this question is really asking.

The relevant article from Wikipedia states:

In simple terms: Preemptive multitasking involves the use of an interrupt mechanism which suspends the currently executing process and invokes a scheduler to determine which process should execute next. Therefore, all processes will get some amount of CPU time at any given time.

Of course, more advanced CPU's, such as the i8086/88 and the Motorola 68000, had additional architectural features to help OS developers to implement a practical multitasking environment. The solutions for 8-bit uP (there have been many valiant attempts) tend to be proof-of-concept novelties. The task switching performance and memory constraints usually make it too slow to be practical for real-world application usage.

A notable exception to the above is OS-9, which provides a quite functional Unix-like multitasking OS for the 6809 microprocessor. It can work with only 64KiB, but excels on a system with more memory. OS-9 is the main reason that Tandy Color Computer 3 users frequently expanded these 8-bit systems to 512KiB in the late 1980's.

| improve this answer | |
  • Come on, MP/M did work quite nice on Z80 boxes. – Raffzahn Apr 9 at 8:15
  • 1
    @Raffzahn Fair enough. I feel like it didn't come into its own until Concurrent on the x86 with practical task switching and PC-MODE. Whereas OS-9 was a little more of an exception with peaking in popularity on an inexpensive 8-bit system people bought at their local shopping mall. Personal experience influenced? Absolutely. – Brian H Apr 9 at 12:26
7

Not simultaneously, as it only has one CPU, but it has a 50 Hz timer interrupt, so it can do time sharing processing, provided that programs are well behaved, as there is no memory protection and no supervisor mode in the CPU.

As a proof of concept, I've written a small task scheduler and tasks that are executed in a time sharing fashion (with a 20ms quantum)

; (c)2020 Miguel Angel Rodriguez Jodar, for Retrocomputing (Stack Exchange)
; Demonstration of a very simple scheduler and two tasks running on the ZX Spectrum
; Licensed under GPL 3.0

        org 0c0e0h

Main:
        halt   ;let a whole interrupt happen so we have plenty of time until the next one
        di
        ld a,0c0h
        ld i,a
        im 2   ;set scheduler interrupt
        ei
        ld sp,StackTask1Top    ;load task1
        jp Task1               ;and execute it

        org 0c0ffh
        dw Scheduler

Scheduler:
        push af  ;save current process state it its stack
        push bc
        push de
        push hl
        exx
        push af
        push bc
        push de
        push hl
        exx
        push ix
        push iy

        ld hl,0
        add hl,sp
        push hl  ;get current stack pointer (just interrupted task)

        ld hl,(CurrentProc)
        inc hl
        pop de
        ld (hl),e
        inc hl
        ld (hl),d  ;and store it into process table
        inc hl

        ld a,(hl)  ;check if end of processes in table
        or a
        jr nz,NotEndOfTasks
        ld hl,TaskTable   ;if so, start from the beginning of the table

NotEndOfTasks:
        ld (CurrentProc),hl  ;select next process
        inc hl
        ld e,(hl)
        inc hl
        ld d,(hl)
        ex de,hl
        ld sp,hl             ;load its stack pointer

        pop iy               ;and jump to it, restoring its state
        pop ix
        exx
        pop hl
        pop de
        pop bc
        pop af
        exx
        pop hl
        pop de
        pop bc
        pop af
        ei
        ret

;----------------------------------------------------------
;TASK1: CALCULATE AND PRINT 16-BIT PRIME NUMBERS
;----------------------------------------------------------

Task1:
        ld ix,DataTask1   ;environment for this task (pointers for PRINT routine)
TryNextNumber:
        ld bc,2           ;BC holds the current divisor to try with
TryDivisor:
        ld hl,(NextCandidate)
        ld d,b
        ld e,c
KeepSub:
        or a
        sbc hl,de
        jp m,NotDivisible
        jp z,NotPrime
        jp KeepSub
NotDivisible:
        inc bc
        or a
        ld hl,(NextCandidate)
        sbc hl,bc
        sbc hl,bc
        jp m,ItsPrime
        jp TryDivisor
NotPrime:
        ld hl,(NextCandidate)
        inc hl          ;HL is not prime, try the next odd number
        inc hl
        ld (NextCandidate),hl
        jp TryNextNumber
ItsPrime:
        ld hl,(NextCandidate)
        ld (ix+SCRROW),6    ;print number at position 6,14
        ld (ix+SCRCOL),14
        call CalculateScrPos
        call PrintNumberHL
        jp NotPrime

NextCandidate:
        dw 3   ;we begin with 3 to test if prime


;----------------------------------------------------------
;TASK2: print character set
;----------------------------------------------------------

Task2:  ld ix,DataTask2
        ld a,' '
AnotherChar:
        call PrintCharA   ;just print the character set, from ASCII 32
        inc a
        jp p,AnotherChar  ;to ASCII 127 and start over again
        ld a,' '
NotResetChar:
        jp AnotherChar


;----------------------------------------------------------
PrintNumberHL:
        ld bc,10000
        call Div
        ld a,d
        add a,'0'
        call PrintCharA
        ld bc,1000
        call Div
        ld a,d
        add a,'0'
        call PrintCharA
        ld bc,100
        call Div
        ld a,d
        add a,'0'
        call PrintCharA
        ld bc,10
        call Div
        ld a,d
        add a,'0'
        call PrintCharA
        ld a,l
        add a,'0'
        call PrintCharA
        ret
Div:
        ld d,0
KeepDiv:
        or a
        sbc hl,bc
        jp m,Negative
        push af
        inc d
        pop af
        ret z
        jp KeepDiv
Negative:
        add hl,bc
        ret

PrintCharA:
        push af
        push bc
        push de
        push hl

        ld l,a
        ld h,0
        ld a,(ix+SCRPOS)
        or (ix+SCRPOS+1)
        call z,CalculateScrPos
        ld e,(ix+SCRPOS)
        ld d,(ix+SCRPOS+1)
        push de

        add hl,hl
        add hl,hl
        add hl,hl
        ex de,hl
        push hl
        ld hl,(23606)
        add hl,de
        pop de
        ld b,8
LoopPrintScan:
        ld a,(hl)
        ld (de),a
        inc hl
        inc d
        djnz LoopPrintScan

        pop hl
        ld a,(ix+SCRCOL)
        inc a
        cp (ix+ENDCOL)
        jp z,NotEndColumn
        jp nc,EndColumn
NotEndColumn:
        ld (ix+SCRCOL),a
        inc hl
        ld (ix+SCRPOS),l
        ld (ix+SCRPOS+1),h
        jp EndPrintA
EndColumn:
        ld a,(ix+BGCOL)
        ld (ix+SCRCOL),a

        ld a,(ix+SCRROW)
        inc a
        cp (ix+ENDROW)
        jp z,NotEndRow
        jp nc,EndRow
NotEndRow:
        ld (ix+SCRROW),a
        jp CalcAndEnd
EndRow: ld a,(ix+BGROW)
        ld (ix+SCRROW),a

CalcAndEnd:
        call CalculateScrPos
EndPrintA:
        pop hl
        pop de
        pop bc
        pop af
        ret

CalculateScrPos:  ; HL = (row/8)<<11 + (row%8)<<5 + col
        push af
        push hl
        ld a,(ix+SCRROW)
        and 0f8h
        or 40h
        ld h,a
        ld a,(ix+SCRROW)
        and 7
        sla a
        sla a
        sla a
        sla a
        sla a
        or (ix+SCRCOL)
        ld l,a
        ld (ix+SCRPOS),l
        ld (ix+SCRPOS+1),h
        pop hl
        pop af
        ret

;Table used by the scheduler to get the stack pointer for
;the next task to execute.
;The end of the table is marked by a processor with ID 0
TaskTable:
        db 1  ;ID
        dw StackTask2Top-20   ;stack pointer
        db 2  ;ID
        dw StackTask2Top-20   ;stack pointer
        db 0  ;ID (end of table)
        dw 0  ;null stack

CurrentProc:
        dw TaskTable  ;current position within TaskTable

        ds 64
StackTask1Top:
        dw 0

        ds 64
StackTask2Top:
        dw Task2

BGROW   equ 0
ENDROW  equ 1
BGCOL   equ 2
ENDCOL  equ 3
SCRROW  equ 4
SCRCOL  equ 5
SCRPOS  equ 6

DataTask1:
        db 0,11,0,31   ;min row, max row, min column, max column. Window of (0,0,31,11)
        db 0,0         ;current row,col position
        dw 0           ;screen memory address of such position

DataTask2:
        db 12,22,2,28  ;Text window (12,2,22,28)
        db 12,2
        dw 0

        end Main
| improve this answer | |
  • Wow, thanks, running to test, many thanks for your response – Marcelo Nunes Apr 12 at 14:56
3

Yes it is.

Any processor is capable of multithreading if software is written to implement multithreading.

Of course the Spectrum OS doesn't support it. You would have to replace the OS with a multithreading executive of your own. This is not as complex as you might think if you are only attempting to produce a small controller of some sort. You could treat this as a challenge to learn how multithreading works.

Note that multithreading is NOT the same as multitasking. So references to implementing multitasking on the Spectrum will not help you. Similarly, you only need a single processor. I suspect the Spectrum is a good machine to try this on. Although I've never tried it on the Spectrum myself, I've done it on other machines of that era.

| improve this answer | |

Not the answer you're looking for? Browse other questions tagged or ask your own question.