多线程是关于创建程序中的多个执行路径同时运行的错觉。在今天的多核计算机上,如果线程数保持在限制范围内,这不再是一种错觉。
通往多线程的不同道路
在抢占式多任务模型中,时间片用完会触发线程切换。切换是从正在运行的线程外部启动的。
在我编写的多线程模块中,没有运行线程的批准和协作是无法进行切换的。是正在运行的线程决定在哪里而不是何时可以进行切换。为此,程序员必须将call
s 插入到函数MaybeYieldThread中策略性选择的线程位置。循环是这样做的好地方。如果在这样的调用时刻,时间片还没有过去,那么调用将立即返回。如果时间片已经过去,那么MaybeYieldThread暂时表现得像一个真实的YieldThread和切换发生。
这种方法的主要优点是它可以避免许多您通常会使用同步对象(如互斥锁、信号量或临界区)的竞争条件。您将call MaybeYieldThread
指令插入到线程安全的地方,就是这样!
主要特点
多线程功能编码在单个源文件mtModule.INC中,您可以将其包含在您喜欢的任何应用程序中。
- 占地面积小。包含文件少于 600 字节。
- 非常快。线程开关很便宜。
- 广泛的允许堆栈大小。它们的范围可以从 128 到 65536 字节,以 16 字节为增量。
- 最佳内存使用。很少的开销和一个合并空闲块的分配器。
- 可选择的时间片。时间片的范围可以从 1 到 55 毫秒。(55 使用标准 54.925494 毫秒)
- 循环调度程序。
- 只有几个,易于使用的功能。
- 非常慷慨的参数传递方案。api本身不用作arg的每个(整数)寄存器确实是线程第一次调用的参数。
接口说明
我提出的 api 很小,但我相信它提供了 DOS 程序可能需要的所有多线程功能……在某些时候,我已经实现了线程句柄、线程优先级和线程间通信等功能。回想起来并牢记“少即是多”的说法,我很高兴我删除了所有这些。
这一切都从call
to BeginSessionThread开始。您定义将放置所有线程堆栈的会话内存的边界,定义要使用的时间片,并指向第一个线程,如果没有遇到错误,则立即接收控制权。
第一个线程要做的事情之一是使用CreateThread创建额外的线程。您提供的是另一个线程的代码地址以及您希望为其堆栈使用的内存量。
一旦线程启动并运行,它们可以使用YieldThread放弃控制权以支持下一个线程,使用MaybeYieldThread当且仅当它们正在运行的时间片已经过去时放弃控制权,并使用SleepThread放弃控制并从计划中删除自己,直到请求的持续时间结束。
如果一个线程已经超过了它的目的,一个call
(或jmp
)到ExitThread或一个简单的ret
指令(当然来自平衡堆栈!)从调度程序中永久删除线程并将其堆栈占用的内存返回到空闲会话内存池.
当不再需要多线程时,EndSessionThreadcall
的(或jmp
)将控制权返回到会话开始处正下方的指令(指令)。可以传递退出代码。
或者,从最后一个活动线程退出也将结束会话,但在这种情况下,退出代码将为零。call BeginSessionThread
为了暂停多线程会话,您可以调用StopSessionThread。它将定时器频率重置为标准的 18.2 Hz 并冻结所有挂起的 SleepTimes。要恢复多线程会话,只需要一个call
到ContSessionThread。暂停会话是在不影响睡眠时间的情况下暂时暂停程序的一种方法。而且,如果您想执行子程序甚至启动嵌套的多线程会话,则必须暂停当前会话才能成功。
api 快速参考
BeginSessionThread
Input
BX timeslice in milliseconds [1,55]
CX requested stacksize for first thread
DX near address of first thread
SI para address begin session memory
DI para address end session memory
-- everything else is user defined parameter
Output
CF=0 Session has ended, AX is SessionExitcode
CF=1 'Insufficient memory'
'Invalid stacksize'
'Invalid timeslice'
--------------------------------------
CreateThread
Input
CX requested stacksize for thread
DX near address of thread
-- everything else is user defined parameter
Output
CF=0 OK
CF=1 'Invalid stacksize'
'Out of memory'
--------------------------------------
SleepThread
Input
CX is requested duration in milliseconds
Output
none
--------------------------------------
MaybeYieldThread
Input
none
Output
none
--------------------------------------
YieldThread
Input
none
Output
none
--------------------------------------
ExitThread
Input
none
Output
none
--------------------------------------
EndSessionThread
Input
CX is SessionExitcode
Output
none
--------------------------------------
StopSessionThread
Input
none
Output
none
--------------------------------------
ContSessionThread
Input
none
Output
none
--------------------------------------
一些兴趣点
线程不改变SS
段寄存器并且它在堆栈上留下大约 80 个字节供mtModule.INC使用是强制性的。
为了获得最佳的“抢占性”,您不应过于稀疏地使用MaybeYieldThread 。另一方面,出于效率原因,您可能不应该在紧密循环中使用MaybeYieldThread 。
; mtModule.INC Multithreading in DOS (c) 2020 Sep Roland
; ------------------------------------------------------
; assemble with FASM, compatible with CMD and DOSBox
; Functions:
; BeginSessionThread(BX,CX,DX,SI,DI,..) -> AX CF
; CreateThread(CX,DX,..) -> CF
; SleepThread(CX)
; MaybeYieldThread()
; YieldThread()
; ExitThread()
; EndSessionThread(CX)
; StopSessionThread()
; ContSessionThread()
; Session header:
; +0 wSessionHighMem
; +2 wSessionNumberOfThreads
; +4 dwSessionParentStackptr
; +8 wSessionTickVarStep
; +10 wSessionMicroTimeslice
; +12 wSessionTickVar
; Thread header:
; +0 wThreadLowMem
; +2 wThreadStacksize
; +4 wThreadStatus: DEAD/FREE (-1), AWAKE (0), ASLEEP (1+)
; +6 wThreadStackptr
; --------------------------------------
; IN (bx=0,cx,dx,ss:si,fs) OUT (ax,CF) MOD (cx,si,di,bp,ds,es)
mtAlloc:cmp cx, 4096 ; Max 64KB stack
ja .NOK
cmp cx, 8 ; Min 128 bytes stack
jb .NOK
; Find a free alloc that is big enough
mov ax, fs
inc ax ; Skipping session header
.a: mov ds, ax
cmp [bx+4], bx ; ThreadStatus
jge .b ; Is occupied
mov bp, [bx+2] ; ThreadStacksize (size of free alloc)
sub bp, cx
jae .OK
.b: add ax, [bx+2] ; ThreadStacksize
cmp ax, [fs:bx] ; SessionHighMem
jb .a
.NOK: stc
ret
.OK: je .c ; Tight fit, no split up
; Init header of a free alloc
add ax, cx
mov ds, ax
mov [bx], fs ; ThreadLowMem
mov [bx+2], bp ; ThreadStacksize
mov word [bx+4], -1 ; ThreadStatus = FREE
sub ax, cx
mov ds, ax
; Init thread header
.c: mov [bx], fs ; ThreadLowMem
mov [bx+2], cx ; ThreadStacksize
mov [bx+4], bx ; ThreadStatus = AWAKE
imul di, cx, 16 ; -> DI is total stacksize in bytes
sub di, (32+8+4)+2+2 ; Initial items that go on this stack
mov [bx+6], di ; ThreadStackptr
; Init thread stack
mov es, ax
mov cx, (32+8+4)/2 ; GPRs, SRegs, EFlags
cld
rep movs word [di], [ss:si]
mov [di], dx ; ThreadAddress
mov word [di+2], ExitThread
inc word [fs:bx+2] ; SessionNumberOfThreads
clc
ret
; --------------------------------------
; IN (bx,cx,dx,si,di,..) OUT (ax,CF)
; BX timeslice in milliseconds [1,55] (55 uses standard 54.925494 msec)
; CX requested stacksize for first thread, DX near address of first thread
; SI para address begin session memory, DI para address end session memory
;
; CF=0 Session has ended, AX is SessionExitcode
; CF=1 'Insufficient memory' or 'Invalid stacksize' or 'Invalid timeslice'
BeginSessionThread:
pushfd ; '..' Every register is considered
push ds es fs gs ; parameter on the first invocation
pushad ; of the thread
; Test parameters
mov bp, di ; SessionHighMem
sub bp, si ; ThreadLowMem
jbe mtFail
dec bp
jz mtFail
dec bx ; Timeslice in msec
cmp bx, 55
jnb mtFail
inc bx
; Turn MilliTimeslice BX into TickVarStep AX and MicroTimeslice CX
mov ax, 65535 ; Standard step is 'chain always'
mov cx, 54925 ; Standard slice is 54925.494 microsec
cmp bx, 55
je .a
push dx ; (1)
mov ax, 1193180 Mod 65536 ; TickVarStep = (1193180 * BX) / 1000
mul bx ; BX = [1,54]
imul cx, bx, 1193180/65536
add dx, cx
mov cx, 1000
div cx ; -> AX = {1193, 2386, ..., 64431}
imul cx, bx ; -> CX = {1000, 2000, ..., 54000}
pop dx ; (1)
; Init session header
.a: xor bx, bx ; CONST
mov ds, si ; -> DS = Session header
mov [bx], di ; SessionHighMem
mov [bx+2], bx ; SessionNumberOfThreads = 0
mov [bx+4], sp ; SessionParentStackptr
mov [bx+6], ss
mov [bx+8], ax ; SessionTickVarStep
mov [bx+10], cx ; SessionMicroTimeslice
;;mov [bx+12], bx ; SessionTickVar = 0
; Init header of a free alloc
mov [bx+16], ds ; ThreadLowMem
mov [bx+18], bp ; ThreadStacksize, all of the session
mov word [bx+20], -1 ; ThreadStatus = FREE memory
; Create first thread
mov fs, si ; ThreadLowMem -> FS = Session header
mov si, sp ; -> SS:SI = Initial registers
mov cx, [ss:si+24] ; pushad.CX
call mtAlloc ; -> AX CF (CX SI DI BP DS ES)
jc mtFail
mov [cs:mtTick+5], fs ; ThreadLowMem
mov [cs:mtChain+3], cs ; Patch far pointer
call mtSwap ; Hook vector 08h/1Ch
jmp mtCont
; --------------------------------------
; IN (ss:sp)
mtFail: popad ; Return with all registers preserved
pop gs fs es ds ; to caller
popfd
stc
ret
; --------------------------------------
; IN (cx,dx,..) OUT (CF)
; CX requested stacksize for thread, DX near address of thread
;
; CF=0 OK
; CF=1 'Invalid stacksize' or 'Out of memory'
CreateThread:
pushfd ; '..' Every register is considered
push ds es fs gs ; parameter on the first invocation
pushad ; of the thread
xor bx, bx ; CONST
mov fs, [ss:bx] ; ThreadLowMem -> FS = Session header
mov si, sp ; -> SS:SI = Initial registers
; Coalescing free blocks
mov ax, fs
inc ax
.a: mov ds, ax ; -> DS = Thread header
mov bp, [bx+2] ; ThreadStacksize
cmp [bx+4], bx ; ThreadStatus
jge .c ; Is occupied
mov es, ax
.b: add ax, bp ; BP is size of a free alloc
cmp ax, [fs:bx] ; SessionHighMem
jnb .d
mov ds, ax
mov bp, [bx+2] ; ThreadStacksize
cmp [bx+4], bx ; ThreadStatus
jge .c
add [es:bx+2], bp ; ThreadStacksize, BP is size of
jmp .b ; the free alloc that follows
.c: add ax, bp ; BP is size of an actual thread stack
cmp ax, [fs:bx] ; SessionHighMem
jb .a
.d: call mtAlloc ; -> AX CF (CX SI DI BP DS ES)
jc mtFail
; --- --- --- --- --- --- --
; IN (ss:sp)
mtFine: popad ; Return with all registers preserved
pop gs fs es ds ; to caller
popfd
clc
ret
; --------------------------------------
; IN (cx) OUT ()
; CX is requested duration in msec
SleepThread:
pushf
pusha
push ds
xor bx, bx ; CONST
mov ds, [ss:bx] ; ThreadLowMem -> DS = Session header
mov ax, 1000 ; TICKS = (CX * 1000) / MicroTimeslice
mul cx
mov cx, [bx+10] ; SessionMicroTimeslice
shr cx, 1 ; Rounding to nearest
adc ax, cx
adc dx, bx
div word [bx+10] ; SessionMicroTimeslice
mov [ss:bx+4], ax ; ThreadStatus = TICKS
pop ds
popa
popf
jmp YieldThread
; --------------------------------------
mtTick: push ds ; 1. Decrement all sleep counters
pusha
xor bx, bx ; CONST
mov ax, 0 ; SMC Start of session memory
mov ds, ax ; ThreadLowMem -> DS = Session header
mov cx, [bx+8] ; SessionTickVarStep
stc
adc [bx+12], cx ; SessionTickVar
pushf ; (1)
mov dx, [bx] ; SessionHighMem
inc ax
.a: mov ds, ax ; -> DS = Thread header
mov cx, [bx+4] ; ThreadStatus
dec cx
js .b ; AX was [-1,0], ergo not ASLEEP
mov [bx+4], cx ; ThreadStatus
.b: add ax, [bx+2] ; ThreadStacksize -> End current stack
cmp ax, dx
jb .a
mov byte [cs:$+23], 90h ; 2. Turn 'MaybeYield' into 'Yield'
popf ; (1)
popa
pop ds
jc mtChain
push ax
mov al, 20h
out 20h, al
pop ax
iret
mtChain:jmp far 0:mtTick ; 3. Chain to original vector 08h/1Ch
; --------------------------------------
; IN () OUT ()
MaybeYieldThread:
ret ; SMC {90h=nop,C3h=ret}
; --- --- --- --- --- --- --
; IN () OUT ()
YieldThread:
mov byte [cs:$-1], 0C3h ; Back to 'MaybeYield'
pushfd ; Save context current thread
push ds es fs gs
pushad
xor bx, bx ; CONST
mov ax, ss ; Begin current stack
mov ds, ax ; -> DS = Thread header
mov [bx+6], sp ; ThreadStackptr
mov fs, [bx] ; ThreadLowMem -> FS = Session header
sti ; Guard against every thread ASLEEP!
.a: add ax, [bx+2] ; ThreadStacksize -> End current stack
cmp ax, [fs:bx] ; SessionHighMem
jb .b
mov ax, fs ; Session header
inc ax ; Stack lowest thread
.b: mov ds, ax
cmp [bx+4], bx ; ThreadStatus
jne .a ; Is DEAD/FREE (-1) or ASLEEP (1+)
; --- --- --- --- --- --- --
; IN (ax,bx=0)
mtCont: mov ss, ax
mov sp, [ss:bx+6] ; ThreadStackptr
popad ; Restore context new current thread
pop gs fs es ds
popfd
ret
; --------------------------------------
; IN () OUT ()
ExitThread:
xor bx, bx ; CONST
dec word [ss:bx+4] ; ThreadStatus = DEAD/FREE
mov ds, [ss:bx] ; ThreadLowMem -> DS = Session header
dec word [bx+2] ; SessionNumberOfThreads
jnz YieldThread ; Not exiting from the sole thread
xor cx, cx ; SessionExitcode
; --- --- --- --- --- --- --
; IN (cx) OUT (ax,CF=0)
EndSessionThread:
call mtSwap ; Unhook vector 08h/1Ch
xor bx, bx ; CONST
mov ds, [ss:bx] ; ThreadLowMem -> DS = Session header
lss sp, [bx+4] ; SessionParentStackptr
mov [esp+28], cx ; pushad.AX, SessionExitcode
jmp mtFine
; --------------------------------------
; IN () OUT ()
StopSessionThread:
ContSessionThread:
push ax
mov ax, [ss:0000h] ; ThreadLowMem -> AX = Session header
mov [cs:mtTick+5], ax ; ThreadLowMem (In case there's been a
pop ax ; nested session)
; --- --- --- --- --- --- --
; IN () OUT ()
mtSwap: push ds
pushad
xor bx, bx ; CONST
mov ds, bx ; -> DS = IVT
mov ax, [046Ch] ; BIOS.Timer
.Wait: cmp ax, [046Ch]
je .Wait
cli
mov ds, [cs:mtTick+5] ; ThreadLowMem -> DS = Session header
mov [bx+12], bx ; SessionTickVar = 0
mov dx, [bx+8] ; SessionTickVarStep
mov ds, bx ; -> DS = IVT
mov bl, 1Ch*4 ; BH=0
inc dx ; SessionTickVarStep
jz .Swap
dec dx
mov bl, 08h*4 ; BH=0
mov ax, cs
cmp [cs:mtChain+3], ax
je .Hook
.Unhook:xor dx, dx
.Hook: mov al, 34h
out 43h, al
mov al, dl
out 40h, al
mov al, dh
out 40h, al
.Swap: mov eax, [bx]
xchg [cs:mtChain+1], eax
mov [bx], eax ; Hook/Unhook vector 08h/1Ch
sti
popad
pop ds
ret
; --------------------------------------
一个示例应用程序
下一个演示程序使用上述 api 中可用的每个函数。它的唯一目的是演示如何使用 api 函数,仅此而已。
尝试不同的时间片很容易,因为您可以在命令行上指定时间片的长度(以毫秒为单位)。
该程序在真正的实地址模式和仿真下(Windows CMD 和DOSBox)运行良好。
; mtVersus.ASM Multithreading in DOS (c) 2020 Sep Roland
; ------------------------------------------------------
; assemble with FASM, compatible with CMD and DOSBox
DefaultTimeslice=55 ; [1,55]
ORG 256
mov sp, $
cld
; Was timeslice specified on commandline ?
xor cx, cx ; RequestedTimeslice
mov si, 0081h ; Commandline
Skip: lodsb
cmp al, " "
je Skip
cmp al, 9
je Skip
Digit: sub al, "0"
jb Other
cmp al, 9
ja Other
cbw
imul cx, 10 ; Reasonably ignoring overflow
add cx, ax
lodsb
jmp Digit
Other: mov bx, DefaultTimeslice
cmp cx, 1
jb Setup
cmp cx, 55
ja Setup
mov bx, cx
Setup: mov di, [0002h] ; PSP.NXTGRAF -> end of session memory
lea si, [di-128] ; 2KB session memory (11 threads)
mov dx, Main
mov cx, 8 ; 128 bytes stack
mov bp, MsgCO
call BeginSessionThread ; -> AX CF
jc Exit
mov bp, MsgPE
call BeginSessionThread ; -> AX CF
;;;jc Exit
Exit: mov ax, 4C00h ; DOS.Terminate
int 21h
; --------------------------------------
; IN (bp) ; BP=ModeOfOperation
Main: mov dx, bp ; Displaying title
mov ah, 09h ; DOS.PrintString
int 21h
mov di, EOF ; Preparing output string
mov cx, 79
mov al, " "
rep stosb
mov word [di], 240Dh ; CR and '$'
mov di, EOF+6 ; Creating 10 counting threads
mov dx, Count
mov cx, 8 ; 128 bytes stack
.a: mov byte [di], "0"
call CreateThread ; -> CF
jc EndSessionThread ; CX=8
add di, 8
cmp di, EOF+79
jb .a
mov byte [Flag], 0
mov dx, 10 ; Sleep while counters run (10 sec)
.b: mov cx, 1000
call SleepThread
mov ah, 01h ; BIOS.TestKey
int 16h ; -> AX ZF
jz .c
mov ah, 00h ; BIOS.GetKey
int 16h ; -> AX
call StopSessionThread
mov ah, 00h ; BIOS.GetKey
int 16h ; -> AX
call ContSessionThread
.c: dec dx
jnz .b
not byte [Flag] ; Forces all other threads to exit
call YieldThread
; Exiting from the sole thread == EndSessionThread
mov dl, 10
mov ah, 02h ; DOS.PrintChar
int 21h
ret ; == ExitThread
; --------------------------------------
; IN (di,bp) ; DI=Counter, BP=ModeOfOperation
Count: mov si, di ; Position of the ones in our counter
.a: mov al, [si]
inc al
cmp al, "9"
jbe .b
mov byte [si], "0"
dec si
cmp byte [si], " "
jne .a
mov al, "1"
.b: mov [si], al
mov dx, EOF
mov ah, 09h ; DOS.PrintString
int 21h
cmp bp, MsgPE
je .PE
.CO: call YieldThread
cmp byte [Flag], 0
je Count
jmp ExitThread
.PE: call MaybeYieldThread
cmp byte [Flag], 0
je Count
ret ; == ExitThread
; --------------------------------------
MsgCO: db 13, 10, '10 seconds of cooperative multithreading '
db 'using YieldThread():', 13, 10, '$'
MsgPE: db 13, 10, '10 seconds of preemptive multithreading '
db 'using MaybeYieldThread():', 13, 10, '$'
Flag: db 0
; --------------------------------------
INCLUDE 'mtModule.INC'
; --------------------------------------
EOF:
; --------------------------------------