您的位置:首页 > 其它

实验11 多任务和多线程

2008-04-12 10:28 162 查看

实验11

保护模式支持多任务,能够快速地进行任务切换和保护任务环境。在Windows操作系统中,它能同时运行若干个应用程序(独立运行的程序又称之为进程),每一个进程作为一个任务;在一个进程中,它又可以分成若干个独立的执行流,称之为线程。

11.1 多任务及其调度

下面给出一个用于演示任务切换的实例。该程序在保护模式下建立2个任务,分别在屏幕上显示单个字符和寄存器的值。涉及的内容包括:直接通过TSS段的任务切换,通过任务门的任务切换,任务内特权级的变换及参数传递。

1. 实现步骤

第一个任务称为临时任务,另一个任务称为演示任务。临时任务的代码位于TempCodeSeg段,从标号Virtual处开始执行;演示任务的代码位于DemoCodeSeg段,从标号DemoBegin处开始执行。演示任务还调用了SubRSeg段中的SubRB子程序。
程序执行过程如下:
(1) 实模式下初始化(Start);
(2) 切换到保护模式(JUMP16 <TempCode_Sel>,<OFFSET Virtual>指令);
(3) 设置TR对应临时任务,特权级为0(Virtual);
(4) 直接切换到演示任务,演示任务的特权级为2(JUMP16 DemoTSS_Sel,0);
(5) 把入口参数压入堆栈,经调用门进入显示信息子程序,显示信息子程序的特权级为0(DemoBegin);
(6) 从堆栈中取出入口参数并处理(SubRB);
(7) 从显示信息子程序返回特权级为2的演示代码段(ret 8);
(8) 经任务门切换到特权级为0的临时任务(JUMP32 ToTempT_Sel,0);
(9) 循环执行(4)~(8)任务切换5次(loop指令)。
(10) 切换到实模式(JUMP16 <SEG Real>,<OFFSET Real>);
(11) 实模式下的恢复工作(Real)。
程序中包括以下内容:
(1) 实模式下的数据段(RDataSeg)。其中包括全局描述符表GDT。
(2) 演示任务的LDT段(DemoLDTSeg)。
(3) 演示任务的TSS(DemoTSSSeg)。
(4) 演示任务的0级和2级堆栈段(DemoStack0Seg、DemoStack2Seg)。
(5) 演示任务数据段(DemoDataSeg)。
(6) 子程序代码段(SubRSeg)。
(7) 演示任务代码段(DemoCodeSeg)。
(8) 临时任务的TSS段(TempTSSSeg)。
(9) 临时任务的代码段(TempCodeSeg)。
(10) 实模式下的堆栈段(RStackSeg)。
(11) 实模式下的代码段(RCodeSeg)。
在DemoLDT中,含有以下局部描述符,这些描述符只能被临时任务所使用:
(1) 演示任务的0级和2级堆栈段描述符(DemoStack0、DemoStack2)
(2) 代码段描述符(DemoCode)
(3) 数据段描述符(DemoData)
(4) LDT所在数据段的描述符(ToDLDT)
(5) 临时任务TSS所在数据段的描述符(ToTTSS)
(6) 指向子程序的调用门(ToSubR)
(7) 指向临时任务的任务门(ToTempT)。
其中,在定义前5项描述符时,其基地址为各个段的实模式段基值(16位)。因此,调用InitLDT子程序,将段基值乘以16以后,得到这些段的物理地址(20位),再存放到BaseH、BaseM、BaseL字段中。
GDT中的DemoLDTab、DemoTSS、TempTSS、TempCode、SubR也由InitGDT子程序进行了同样处理。

2. 从临时任务切换到演示任务

TR未在实模式下设置,切换到保护模式后,要把指向临时任务TSS描述符的选择符(TempTSS_Sel)装入TR。之后,才能执行任务切换。利用LTR指令为TR赋值时,并不引用TSS的内容,所以临时任务的TSS不需要被初始化。
在任务切换时,把原任务的现场保存到TR所指示的TSS内,然后再把指向目标任务的TSS描述符的选择符装入TR。从临时任务切换到演示任务时,执行“JUMP16 DemoTSS_Sel,0”指令,切换过程包括:把临时任务的执行现场保存到临时任务的TSS中;把演示任务的LDT描述符选择符(DemoLDT_Sel)装载到LDTR;从演示任务的TSS中恢复演示任务的现场等。
在TempTSSSeg中,演示任务TSS的CS字段存放的是DemoCode_Sel,对应的描述符是DemoLDT中的DemoCode,其DPL=2;EIP字段存放的是DemoBegin,所以在切换到演示任务后从DemoBegin开始执行,并且CPL=2。由于使用JMP指令进行任务切换,所以不实施任务链接。

3. 演示任务内的特权级变换和

演示任务采用段间调用指令CALL,通过调用门ToSubR调用子程序SubRB。调用门内的它所指示的子程序代码段描述符(SubR_Sel)的DPL=0,所以在调用过程中就发生了从特权级2到特权级0的变换,同时堆栈也被切换到DemoStack0Seg。
通过堆栈传递了两个参数给子程序SubRB。在把参数压入堆栈时,CPL=2,使用的也是对应特权级2 的堆栈(DemoStack2Seg)。通过调用门进入子程序后,CPL=0,使用0级堆栈。为此,把调用门ToSubR中的DCount字段设置为2,提升到特权级0时,CPU从DemoStack2Seg复制了2个双字参数到DemoStack0Seg。
从子程序SubRB返回时,CPL=0变换为CPL=2,堆栈也回到2级堆栈(DemoStack2Seg)。

4. 从演示任务切换到临时任务

演示任务采用段间转移指令JMP,通过任务门ToTempT切换到临时任务。演示任务的现场被保存到演示任务的TSS;临时任务的现场从临时任务的TSS恢复。
临时任务的挂起点是TempCodeSeg的“inc byte ptr es:[0h]”指令,所以恢复后的临时任务从该指令处开始执行。在恢复到临时任务时,SS、DS、FS、GS被恢复为Normal_Sel,ES被恢复为Video_Sel。

5. 任务现场的保存、恢复

程序在临时任务和演示任务之间进行了多次任务切换。临时任务在进入时,清除屏幕内容,将屏幕左上角(0,0)处的字符设为’0’。
演示任务执行一次,显示EDI的当前值。第1次执行时,EDI的值等于(80*4+50)*2,即000002E4h,在屏幕上位置(4,50)处显示“EDI=000002E4”,之后,EDI增加160,执行下一行。在切换到临时任务时,EDI的值被保存到DemoTSSSeg中。下一次从临时任务切换到演示任务时,EDI从DemoTSSSeg中恢复,所以在屏幕上位置(5,50)处显示“EDI=00000384”。
从临时任务切换到演示任务时,临时任务的ECX值也被保存到TempTSSSeg中,切换回临时任务时,从TempTSSSeg恢复ECX值,所以,循环一共执行5次,每次将屏幕左上角(0,0)处的字符加1。执行结束时,该字符变为’5’。
;程序清单: task.asm(任务切换)
INCLUDE 386SCD.INC

RDataSeg SEGMENT PARA USE16 ;实方式数据段
;全局描述符表
GDT LABEL BYTE
;空描述符
DUMMY Desc <>
;规范段描述符
Normal Desc <0ffffh,,,ATDW,,>
;视频缓冲区段描述符(DPL=3)
VideoBuf Desc <0ffffh,8000h,0bh,ATDW+DPL3,,>
EFFGDT LABEL BYTE
;演示任务的局部描述符表段的描述符
DemoLDTab Desc <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
;演示任务的任务状态段描述符
DemoTSS Desc <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
;临时任务的任务状态段描述符
TempTSS Desc <TempTSSLen-1,TempTSSSeg,,AT386TSS+DPL2,,>
;临时代码段描述符
TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>
;子程序代码段描述符
SubR Desc <SubRLen-1,SubRSeg,,ATCE,D32,>
GDNum = ($-EFFGDT)/8 ;需处理基地址的描述符个数
GDTLen = $-GDT ;全局描述符表长度

VGDTR PDesc <GDTLen-1,> ;GDT伪描述符
SPVar DW ? ;用于保存实方式下的SP
SSVar DW ? ;用于保存实方式下的SS
RDataSeg ENDS

Normal_Sel = Normal-GDT
Video_Sel = VideoBuf-GDT
DemoLDT_Sel = DemoLDTab-GDT
DemoTSS_Sel = DemoTSS-GDT
TempTSS_Sel = TempTSS-GDT
TempCode_Sel = TempCode-GDT
SubR_Sel = SubR-GDT

DemoLDTSeg SEGMENT PARA USE16 ;局部描述符表数据段(16位)
DemoLDT LABEL BYTE ;局部描述符表
;0级堆栈段描述符(32位段)
DemoStack0 Desc <DemoStack0Len-1,DemoStack0Seg,,ATDW,D32,>
;2级堆栈段描述符(32位段)
DemoStack2 Desc <DemoStack2Len-1,DemoStack2Seg,,ATDW+DPL2,D32,>
;演示任务代码段描述符(32位段,DPL=2)
DemoCode Desc <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL2,D32,>
;演示任务数据段描述符(32位段,DPL=3)
DemoData Desc <DemoDataLen-1,DemoDataSeg,,ATDW+DPL3,D32,>
;把LDT作为普通数据段描述的描述符(DPL=2)
ToDLDT Desc <DemoLDTLen-1,DemoLDTSeg,,ATDW+DPL2,,>
;把TSS作为普通数据段描述的描述符(DPL=2)
ToTTSS Desc <TempTSSLen-1,TempTSSSeg,,ATDW+DPL2,,>
DemoLDNum = ($-DemoLDT)/(8) ;需处理基地址的LDT描述符数
;指向子程序SubRB代码段的调用门(DPL=3)
ToSubR Gate <SubRB,SubR_Sel,,AT386CGate+DPL3,>
;指向临时任务Temp的任务门(DPL=3)
ToTempT Gate <,TempTSS_Sel,,ATTaskGate+DPL3,>
DemoLDTLen = $-DemoLDT
DemoLDTSeg ENDS ;局部描述符表段定义结束

DemoStack0_Sel = DemoStack0-DemoLDT+TIL
DemoStack2_Sel = DemoStack2-DemoLDT+TIL+RPL2
DemoCode_Sel = DemoCode-DemoLDT+TIL+RPL2
DemoData_Sel = DemoData-DemoLDT+TIL
ToDLDT_Sel = ToDLDT-DemoLDT+TIL
ToTTSS_Sel = ToTTSS-DemoLDT+TIL
ToSubR_Sel = ToSubR-DemoLDT+TIL+RPL2
ToTempT_Sel = ToTempT-DemoLDT+TIL

DemoTSSSeg SEGMENT PARA USE16 ;任务状态段TSS
DD 0 ;链接字
DD DemoStack0Len ;0级堆栈指针
DW DemoStack0_Sel,0 ;0级堆栈选择符
DD 0 ;1级堆栈指针(实例不使用)
DW 0,0 ;1级堆栈选择符(实例不使用)
DD 0 ;2级堆栈指针
DW 0,0 ;2级堆栈选择符
DD 0 ;CR3
DW DemoBegin,0 ;EIP
DD 0 ;EFLAGS
DD 0 ;EAX
DD 0 ;ECX
DD 0 ;EDX
DD 0 ;EBX
DD DemoStack2Len ;ESP
DD 0 ;EBP
DD 0 ;ESI
DD (80*4+50)*2 ;EDI
DW Video_Sel,0 ;ES
DW DemoCode_Sel,0 ;CS
DW DemoStack2_Sel,0 ;SS
DW DemoData_Sel,0 ;DS
DW ToDLDT_Sel,0 ;FS
DW ToTTSS_Sel,0 ;GS
DW DemoLDT_Sel,0 ;LDTR
DW 0 ;调试陷阱标志
DW $+2 ;指向I/O许可位图
DB 0ffh ;I/O许可位图结束标志
DemoTSSLen = $
DemoTSSSeg ENDS ;任务状态段TSS结束

DemoStack0Seg SEGMENT PARA USE32 ;演示任务0级堆栈段(32位段)
DemoStack0Len = 1024
DB DemoStack0Len DUP(0)
DemoStack0Seg ENDS ;演示任务0级堆栈段结束

DemoStack2Seg SEGMENT PARA USE32 ;演示任务2级堆栈段(32位段)
DemoStack2Len = 512
DB DemoStack2Len DUP(0)
DemoStack2Seg ENDS ;演示任务2级堆栈段结束

DemoDataSeg SEGMENT PARA USE32 ;演示任务数据段(32位段)
Message DB 'EDI=',0
tableH2A DB '0123456789ABCDEF'
DemoDataLen = $
DemoDataSeg ENDS ;演示任务数据段结束

SubRSeg SEGMENT PARA USE32 ;子程序代码段(32位)
ASSUME CS:SubRSeg
SubRB PROC FAR
push ebp
mov ebp,esp
push edi
mov esi,DWORD PTR [ebp+12] ;从0级栈中取出显示串偏移
mov ah,47h ;设置显示属性
SubR1:
lodsb
or al,al
jz SubR2
stosw
jmp short SubR1
SubR2:
mov ah,4eh ;设置显示属性
mov edx,DWORD PTR [ebp+16] ;从0级栈中取出显示值
mov ecx,8
SubR3:
rol edx,4
mov al,dl
and al,0fh
movzx ebx,al
mov al,ds:tableH2A[ebx]
stosw
loop SubR3
pop edi
add edi,160
pop ebp
ret 8
SubRB ENDP
SubRLen = $
SubRSeg ENDS ;子程序代码段结束

DemoCodeSeg SEGMENT PARA USE32 ;演示任务的32位代码段
ASSUME CS:DemoCodeSeg,DS:DemoDataSeg
DemoBegin PROC FAR
;把要复制的参数个数置入调用门
mov BYTE PTR fs:ToSubR.DCount,2
;向2级堆栈中压入参数
push EDI
push OFFSET Message
;通过调用门调用SubRB
CALL32 ToSubR_Sel,0

;通过任务门切换到临时任务
JUMP32 ToTempT_Sel,0
jmp DemoBegin
DemoBegin ENDP
DemoCodeLen = $
DemoCodeSeg ENDS ;演示任务的32位代码段结束

TempTSSSeg SEGMENT PARA USE16 ;临时任务的任务状态段TSS
TempTask TSS <>
DB 0ffh ;I/O许可位图结束标志
TempTSSLen = $
TempTSSSeg ENDS

TempCodeSeg SEGMENT PARA USE16 ;临时任务的代码段
ASSUME CS:TempCodeSeg
Virtual PROC FAR
mov ax,TempTSS_Sel ;装载TR
ltr ax

mov ax,Video_Sel
mov es,ax
mov ax,Normal_Sel
mov ds,ax
mov fs,ax
mov gs,ax
mov ss,ax

xor edi,edi
mov ecx,25*80
mov ax,0720h
cld
rep stosw

mov byte ptr es:[0h], '0'
mov ecx,5
Virtual0:
JUMP16 DemoTSS_Sel,0 ;直接切换到演示任务
inc byte ptr es:[0h]
loop Virtual0

clts ;清任务切换标志
mov eax,cr0 ;准备返回实模式
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
Virtual ENDP
TempCodeSeg ENDS

RStackSeg SEGMENT PARA STACK ;实方式堆栈段
DB 512 DUP (0)
RStackSeg ENDS ;堆栈段结束

RCodeSeg SEGMENT PARA USE16
ASSUME CS:RCodeSeg,DS:RDataSeg,ES:RDataSeg
Start PROC
mov ax,RDataSeg
mov ds,ax
mov SSVar,ss
mov SPVar,sp

cld
call InitGDT ;初始化全局描述符表GDT

call InitLDT ;初始化局部描述符表LDT

lgdt QWORD PTR VGDTR ;装载GDTR并切换到保护方式
cli
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <TempCode_Sel>,<OFFSET Virtual>
Real:
mov ax,RDataSeg ;又回到实方式
mov ds,ax
mov sp,SPVar
mov ss,SSVar
sti
mov ax,4c00h
int 21h
Start ENDP

InitGDT PROC
mov cx,GDNum
mov si,OFFSET EFFGDT
mov bx,16
InitG:
mov ax,[si].BaseL
mul bx
mov WORD PTR [si].BaseL,ax
mov BYTE PTR [si].BaseM,dl
mov BYTE PTR [si].BaseH,dh
add si,8
loop InitG

mov ax,ds
mul bx
add ax,offset GDT
adc dx,0
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
ret
InitGDT ENDP

InitLDT PROC
mov ax,DemoLDTSeg
mov es,ax
mov si,OFFSET DemoLDT
mov cx,DemoLDNum
mov bx,16
InitL:
mov ax,WORD PTR es:[si].BaseL
mul bx
mov WORD PTR es:[si].BaseL,ax
mov BYTE PTR es:[si].BaseM,dl
mov BYTE PTR es:[si].BaseH,dh
add si,8
loop InitL
ret
InitLDT ENDP
RCodeSeg ENDS
END Start

11.2 多线程编程

运行一个可执行文件时,Windows操作系统会创建一个进程,为该进程生成私有内存地址空间(包括代码区、数据区、堆栈区),把磁盘上的可执行文件映射到该空间上。接着,操作系统自动地为该进程创建一个主线程,主线程通常从可执行文件的第一条指令处开始执行。
除了主线程外,在程序中创建新的线程,创建出的线程和主线程在同一个进程环境中执行,共享相同的私有内存地址空间、全局变量、资源、文件句柄等。但是,每一个线程都有一个独立的堆栈。
利用线程能够并发运行的特点,可以将一个应用程序内的具体操作分成若干个线程。例如,一个应用程序可分为3个线程:1个用户接口线程、1个数据采集线程和1个分析线程。这3个线程各自独立运行,在等待用户输入的同时,系统还能够同时进行数据的采集和分析。因此,采用多线程能够提高CPU的使用效率,发挥多CPU、超线程、多核系统的优势。

1. 创建线程

调用CreateThread函数创建新的线程,其参数如下:
CreateThread proto lpThreadAttributes:DWORD,/
dwStackSize:DWORD,/
lpStartAddress:DWORD,/
lpParameter:DWORD,/
dwCreationFlags:DWORD,/
lpThreadId:DWORD
lpThreadAttributes:线程的安全属性,置该值为NULL时,使用缺省的安全属性。
dwStackSize:线程的堆栈大小。如果为0,使用和进程相同大小的堆栈。
lpStartAddress:线程函数的起始地址。
lpParameter:传递给线程的上下文。可以是一个整数,或者一个指向某个结构的指针,通过lpParameter将参数传递给线程。
dwCreationFlags:=0表示创建线程后立即启动;=CREATE_SUSPENDED时,需要调用ResumeThread()函数启动该线程。
lpThreadId:指向一个双字,用于保存线程ID。
线程的执行类似于执行一个函数,执行结束后,线程就被自动地中止。在线程中可以设计一个循环,在条件满足时,线程在循环中执行其功能。

2. 线程同步问题

线程能够共享进程的资源,包括数据区中的全局变量。多个线程存取同一个全局变量,,必须考虑同步的问题。例如,我们创建了2个线程,每个线程在函数中执行以下3条指令:
mov eax,dwCounter
add eax,1
mov dwCounter,eax
dwCounter的初值为0,每个线程各执行一次,最后的结果应该是dwCounter=2。但是,由于线程执行的并发性,其结果并不一定等于2。假如,第1个线程在执行完“mov eax,dwCounter”后,线程被操作系统挂起,切换到第2个线程,那么执行顺序如图11-1所示。

mov eax,dwCounter

add eax,1
mov dwCounter,eax

mov eax,dwCounter
add eax,1
mov dwCounter,eax



图11-1 2个线程共享全局变量的同步问题
第1个线程执行完“mov eax,dwCounter”指令后,eax=0,这时,切换到第2个线程,第2个线程执行时,dwCounter仍然为0,执行完毕后,dwCounter=1。这时,再切换到第1个线程,eax从0增加到1,再保存到dwCounter中,结果dwCounter=1。
上面的问题就是多个线程共享全局变量所带来的同步问题。这样的错误很隐蔽,每次的结果可能都不相同,不容易重现。例如,一个结构有10个成员变量,一个线程在对其赋值时,在更新了前5个成员变量后,被系统挂起,切换到另一个线程。那么这个线程访问这个结构时,前后5个成员变量就会不一致,导致不正确的结果。

3. 互斥锁

互斥锁是一种特殊的信号量。信号量中有一个计数器,信号量有2个操作:“获得”、“释放”。为“获得”信号量,如果信号量的当前值为 0,那么线程就阻塞,直到计数器值大于0为止。之后,该计数器值减 1。“释放”信号量时,该计数器值加 1。
互斥锁是那些值只能为 0 或 1 的信号量。一个互斥锁可以作为一个“令牌”,每次只允许一个线程访问共享的全局变量。互斥锁用于线程之间的互相排斥:在任何一个时刻,只有一个线程可以“拥有”一个互斥锁。
(1)创建互斥锁
CreateMutex()函数可用来创建一个有名或无名的互斥锁对象,其函数原型为:
CreateMutex proto lpMutexAttributes:DWORD,/
bInitialOwner:DWORD,/
lpName:DWORD
lpMutexAttributes:互斥锁的安全属性,置该值为NULL时,使用缺省安全属性。
bInitialOwner:=TRUE时,表示创建后立即获取该互斥锁对象;=FALSE时,仅创建该互斥锁,不获取该互斥锁对象。
lpName:字符串,互斥锁的名字。置该值为NULL时,创建无名互斥锁对象。
如果成功执行,将返回一个互斥锁对象的句柄。
(2)获取互斥锁
WaitForSingleObject()函数对应于信号量的“获得”操作。其原型为:
WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD
hObject :同步对象(互斥锁、信号量、事件等)的句柄。
dwTimeout :等待同步对象变成“有信号”前的最长时间,以毫秒计。当等待的时间超过该值后,WaitForSingleObject函数返回,不再等待。如果想要线程一直等待到同步对象变成“有信号”,该参数设为INFINITE(该值等于0xffffffff)。
(3)释放互斥锁
ReleaseMutex()函数对应于信号量的“释放”操作。其原型为:
ReleaseMutex proto hObject:DWORD
hObject :同步对象(互斥锁、信号量、事件等)的句柄。

4. 线程同步演示程序

在对话框初始化时,创建了2个互斥锁hMutexCounter、hMutexRounds。
(1)线程的创建
按下“开始计数”按钮后,程序创建了10个线程,每个线程的编号为0~9,通过_lParam参数传递到线程函数中。
_ThreadWithSync、_ThreadWithoutSync的功能相同。每个线程中执行的操作等价于:
dwCounter++;
dwCounter--;
每一次的操作作为一轮计数,计数的轮数保存在dwRounds中。
ThreadWithSync在更新dwCounter、dwRounds时,使用互斥锁hMutexCounter、hMutexRounds进行了保护。而ThreadWithoutSync未使用互斥锁。
(2)显示结果
计数开始的时刻报存在dwStartTick中,GetTickCount()函数返回以毫秒为单位的计算机启动后所经过的时间间隔。
计数的速度dwSpeed,即每秒完成的轮数,通过以下公式计算:
(dwRounds * 1000) / (dwCurrentTick-dwStartTick)
dwCurrentTick是当前时刻。
对话框中含有3个编辑框,用于显示dwCounter、dwRounds、dwSpeed的当前值。不论计数过程完成了多少轮,dwCounter的正确结果应该为0。
程序中设定了一个定时器,每隔100毫秒,将上述3个整数显示到编辑框中。
(3)线程结束
dwRunning是一个数组,存放线程是否正在运行的标志。线程x开始执行时,将标志dwRunning[x]设为1,x是线程的编号,从0到9。
按下“停止计数”按钮后,将dwStopFlag标志置为1,线程检查到此标志后,退出循环。在将dwRunning[x]设为0。
调用_WaitThreadStop子程序,该子程序等到所有10个标志全部变为0,即全部10个线程执行结束。
(4)对比
启动thread.exe后,“同步”复选框未选中,按下“开始计数”按钮,这时,创建的10个线程执行的是_ThreadWithoutSync函数,经过一段时间后,按下“停止计数”按钮,结果显示如图11-2所示。
点击“同步”复选框,再按下“开始计数”按钮,这时,10个线程执行的是_ThreadWithSync函数,经过一段时间后,按下“停止计数”按钮,结果显示如图11-3所示。



图11-2 未采用同步的计数结果 图11-3 采用同步后的计数结果
对比上述结果,未采用同步时,10个线程对dwCounter同时更新。停止计数后,dwCounter的值为2。证实了dwCounter的加1、减1操作可以被另一个线程打断,导致结果不正确。
而采用同步之后,dwCounter的值始终为0。由于使用了互斥锁hMutexCounter,dwCounter的加1、减1操作不会被另一个线程打断,结果正确。
从计数速度的不同反映出,采用同步后,WaitForSingleObject()频繁阻塞线程的执行,其计数的速度大幅度降低。
;程序清单: thread.asm(线程同步演示)
.386
.model flat, stdcall
option casemap :none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib

;资源文件中用到的常量
DLG_MAIN equ 1000
IDC_ROUNDS equ 1001
IDC_COUNTER equ 1002
IDC_SPEED equ 1003
IDC_START equ 1004
IDC_STOP equ 1005
IDC_SYNC equ 1006

;创建线程的数目
THREADS equ 10

.data
hInstance dd ? ;当前程序实例
hWinDlg dd ? ;对话框窗口句柄
hWinStart dd ? ;"开始计数"按钮的窗口句柄
hWinStop dd ? ;"停止计数"按钮的窗口句柄
hWinSync dd ? ;"同步"复选框的窗口句柄

dwRunning dd THREADS dup (0) ;10个线程的运行标志,=1:正在运行

dwStopFlag dd 1 ;线程检查此标志,=1时线程自动停止

dwRounds dd ? ;全部线程已完成的轮数
dwCounter dd ? ;计数的当前值

dwStartTick dd ? ;计数开始的时刻(单位:毫秒)
dwCurrentTick dd ? ;当前时刻(单位:毫秒)

szCounterMutex db 'CounterMutex',0 ;第1个互斥锁的名称
szRoundsMutex db 'RoundsMutex',0 ;第2个互斥锁的名称

hMutexCounter dd ? ;为保护dwCounter所设置的互斥锁
hMutexRounds dd ? ;为保护dwRounds所设置的互斥锁

.code
;显示计数的当前值、已完成的轮数、每秒完成的轮数
_ShowCounter proc
local @dwSpeed ;每秒完成的轮数

invoke GetTickCount
mov dwCurrentTick,eax ;dwCurrentTick=GetTickCount()
mov eax,dwRounds
mov ebx,dwCurrentTick
sub ebx,dwStartTick ;ebx=dwCurrentTick-dwStartTick

cmp ebx,0 ;dwCurrentTick=dwStartTick,不做除法
jz div0
mov ecx,1000
mul ecx ;edx:eax=dwRounds*1000
div ebx ;eax=(dwRounds*1000)/ebx
div0:
mov @dwSpeed,eax

;在3个编辑框中分别显示dwCounter,dwRounds,@dwSpeed
invoke SetDlgItemInt,hWinDlg,IDC_COUNTER,dwCounter,FALSE
invoke SetDlgItemInt,hWinDlg,IDC_ROUNDS,dwRounds,FALSE
invoke SetDlgItemInt,hWinDlg,IDC_SPEED,@dwSpeed,FALSE
ret
_ShowCounter endp

;未创建线程时, 允许"开始计数"按钮, 禁止"停止计数"按钮, 允许"同步"复选框
_EnableStart proc
invoke EnableWindow,hWinStart,TRUE
invoke EnableWindow,hWinStop,FALSE
invoke EnableWindow,hWinSync,TRUE
ret
_EnableStart endp

;已创建了线程, 禁止"开始计数"按钮, 允许"停止计数"按钮, 禁止"同步"复选框
_EnableStop proc
invoke EnableWindow,hWinStart,FALSE
invoke EnableWindow,hWinStop,TRUE
invoke EnableWindow,hWinSync,FALSE
ret
_EnableStop endp

;等待10个线程全部运行结束
_WaitThreadStop proc
xor ebx,ebx
.while ebx < THREADS ;ebx(i)从0循环到9
.if dwRunning[ebx*4] == 0 ;第i个线程是否正在运行?
inc ebx ;第i个线程未运行,检查下一个
.else
invoke Sleep,10 ;正在运行,睡眠10ms后继续检查
.endif
.endw
ret
_WaitThreadStop endp

;进行同步的线程, _lParam=线程编号(x=0~9)
_ThreadWithSync proc uses ebx esi edi,_lParam

mov ebx,_lParam
mov dwRunning[ebx*4],1 ;dwRunning[x]=1,线程正在运行

.while dwStopFlag == 0 ;dwStopFlag=1时,退出while循环

;如果其他线程持有hMutexCounter互斥锁,等待该互斥锁被释放
;WaitForSingleObject执行结束后,本线程持有hMutexCounter互斥锁
invoke WaitForSingleObject,hMutexCounter,INFINITE

;以下6条指令位于hMutexCounter互斥锁的保护范围
;这些指令不会被两个线程同时执行
mov eax,dwCounter
add eax,1
mov dwCounter,eax ;dwCounter++

mov eax,dwCounter
sub eax,1
mov dwCounter,eax ;dwCounter--

;释放hMutexCounter互斥锁
invoke ReleaseMutex,hMutexCounter

;用hMutexRounds互斥锁防止2个线程同时更新dwRounds
invoke WaitForSingleObject,hMutexRounds,INFINITE
inc dwRounds
invoke ReleaseMutex,hMutexRounds

.endw

mov ebx,_lParam
mov dwRunning[ebx*4],0 ;dwRunning[x]=0,线程已结束运行
ret
_ThreadWithSync endp

;未进行同步的线程, _lParam=线程编号(x=0~9)
_ThreadWithoutSync proc uses ebx esi edi,_lParam

mov ebx,_lParam
mov dwRunning[ebx*4],1 ;dwRunning[x]=1,线程正在运行

.while dwStopFlag == 0 ;dwStopFlag=1时,退出while循环

;以下6条指令对dwCounter进行更新,其执行过程可能被打断,导致错误
mov eax,dwCounter
add eax,1
mov dwCounter,eax ;dwCounter++

mov eax,dwCounter
sub eax,1
mov dwCounter,eax ;dwCounter--

inc dwRounds ;dwRounds++

.endw

mov ebx,_lParam
mov dwRunning[ebx*4],0 ;dwRunning[x]=0,线程已结束运行
ret
_ThreadWithoutSync endp

;对话框的处理函数
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
local @dwThreadID
local @dwStartAddress

mov eax,wMsg
.if eax == WM_INITDIALOG ;对话框被创建时,发送此消息

mov eax,hWnd
mov hWinDlg,eax ;hWinDlg=hWnd

;获得"开始计数"按钮、"停止计数"按钮、"同步"复选框的窗口句柄
invoke GetDlgItem,hWnd,IDC_START
mov hWinStart,eax
invoke GetDlgItem,hWnd,IDC_STOP
mov hWinStop,eax
invoke GetDlgItem,hWnd,IDC_SYNC
mov hWinSync,eax

;线程还未开始运行, 允许"开始计数"按钮
call _EnableStart

;"同步"复选框的初始状态设为"未选中"
invoke CheckDlgButton,hWnd,IDC_SYNC,BST_UNCHECKED

;创建2个互斥锁
invoke CreateMutex,NULL,FALSE,offset szCounterMutex
mov hMutexCounter,eax
invoke CreateMutex,NULL,FALSE,offset szRoundsMutex
mov hMutexRounds,eax

.elseif eax == WM_COMMAND

mov eax,wParam
.if ax == IDC_START ;"开始计数"按钮被按下
mov dwRounds,0 ;dwRounds=0
mov dwCounter,0 ;dwCounter=0
mov dwStopFlag,0 ;dwStopFlag=0

invoke GetTickCount
mov dwStartTick,eax ;dwStartTick=计数开始时刻

;检查"同步"复选框是否被选中
invoke IsDlgButtonChecked,hWnd,IDC_SYNC
.if eax == BST_CHECKED
;被选中,使用_ThreadWithSync线程
mov @dwStartAddress,offset _ThreadWithSync
.else
;未选中,使用_ThreadWithoutSync线程
mov @dwStartAddress,offset _ThreadWithoutSync
.endif

;ebx=0~9循环,创建10个线程
xor ebx,ebx
.while ebx < THREADS
;@dwStartAddress=线程函数
;ebx=线程函数的_lParam
invoke CreateThread,NULL,0,@dwStartAddress,ebx,/
NULL,addr @dwThreadID
invoke CloseHandle,eax
inc ebx
.endw

;创建定时器,每100毫秒产生一个WM_TIMER消息
invoke SetTimer,hWnd,1,100,NULL

;线程已经开始运行, 允许"结束计数"按钮
call _EnableStop

.elseif ax == IDC_STOP ;"结束计数"按钮被按下

;dwStopFlag=1,线程检查到此标志后不再计数
mov dwStopFlag,1

;删除定时器,不再产生WM_TIMER消息
invoke KillTimer,hWnd,1

;等待10个线程全部结束
call _WaitThreadStop

;显示最后结果
call _ShowCounter

;线程已经全部结束, 允许"开始计数"按钮
call _EnableStart

.endif

.elseif eax == WM_TIMER

;定时器存在时,每100毫秒收到一个WM_TIMER消息
call _ShowCounter ;显示当前结果

.elseif eax == WM_CLOSE

;dwStopFlag=0,线程在运行,不允许关闭对话框
.if dwStopFlag == 1
invoke EndDialog,hWnd,NULL ;结束对话框
.endif

.else

mov eax,FALSE ;消息未被处理, 返回FALSE
ret

.endif

mov eax,TRUE ;消息已被处理, 返回TRUE
ret
_ProcDlgMain endp

_start:
;hInstance=GetModuleHandle(NULL)
invoke GetModuleHandle,NULL
mov hInstance,eax
;创建对话框
invoke DialogBoxParam,eax,DLG_MAIN,NULL,offset _ProcDlgMain,NULL
;进程结束
invoke ExitProcess,NULL
end _start

11.3 X86-64架构简介

CPU的位数,从4位发展到8位、16位、32位、64位。它指的是处理器的数据流宽度,也就是处理器的通用寄存器(General Purpose Register,GPR)可以容纳下的数据位数(bit)。所以说,一个64位CPU实际上就是一个GPR可以容纳64位数的处理器,64位的指令也就是可以操作64位数的指令。
X86-64结构最早由AMD公司开发,它是Intel IA-32架构(Intel Architectur-32 extension,也称作X86架构)的一个扩展,并且与IA-32保持兼容。随后,Intel公司在CPU中进行了同样的扩展,称之为EM64T(Extended Memory 64-bit Technology)。X86-64结构在32位X86指令集的基础上,加入了扩展的64位X86指令集,使处理器在硬件上兼容原来的32位X86软件,并同时支持X86-64的扩展64位计算,且具有64位的寻址能力,使得处理器成为32位/64位兼容的X86芯片。

1. 数据类型的扩展

从32位CPU升级到64位CPU,通用寄存器和CPU内部的数据通道都增加了1倍。CPU所处理的常用数据有整数和地址两种类型,地址型数据可看作是一种特殊类型的整数数据。两种数据都储存在GPR中,并都可以在ALU(算术逻辑单元)种进行计算。除了整数和地址之外,CPU还支持其他两种数据类型:浮点和矢量,这两种类型的数据都有自己的寄存器和执行单元。
表11-2 数据类型的扩展
数据类型
寄存器类型
执行单元
X86位数
X86-64位数
整数
GPR
ALU(算术逻辑单元)
32
64
地址
GPR
ALU(算术逻辑单元)
AGU(地址生成单元)
32
64
浮点
FPR
FPU(浮点处理单元)
64
64
矢量
VR
VPU(矢量处理单元)
128
128
在X86架构中,无符号整数的表示范围为0~232-1,而X86-64架构将这个范围扩充到0~264-1。
在内存寻址方面,X86架构中地址指针为32位,而X86-64架构为64位,可以表示直接寻址更大的内存空间。

2. 扩展的寄存器

(1)64位扩展
X86工业标准是Intel公司在1978年推出的,从最早的8位、16位一直提高到32位。8086处理器,具有4个16位的GPR(AX、BX、CX、DX)和4个16位的寄存器(SI、DI、BP、SP)。在80386中,寄存器扩展到32位,用EAX、EBX、…、ESP表示。
在X86-64中,8个GPR的位数增加到64位,用RAX、RBX、…、RSP表示。
(2)增加寄存器
在X86-64之前,X86工业标准维持着“888”—8个通用寄存器(GPR)、8个浮点寄存器(FPR)和8个SIMD寄存器。在RISC工业标准中,处理器中的寄存器数量要多得多,PowerPC的寄存器每种都有32个。增加寄存器可以使处理器容纳更多的数据,执行单元可以快速处理,降低延迟。同时,更多的寄存器也使得程序员、编译程序可以更灵活地安排指令,降低彼此之间的依赖关系,避免流水线中的堵塞。
在AMD的X86-64架构中,将GPR和SIMD寄存器的数量加倍。当处理器工作在64位模式下时,处理器具有16个GPR,以及8个新的SIMD寄存器(支持SSE/SSE2)。

3. 运行模式

X86-64架构具有传统IA-32模式和IA-32e扩展模式。激活EM64T后,进入IA-32e扩展模式。
在传统IA-32模式(“Legacy”)下,处理器就像标准的X86处理器一样工作,可以运行32位操作系统和32位代码,不使用X86-64新增加的各项功能。
在IA-32e扩展模式(“Long Mode”)下,又分为兼容模式和64位模式。兼容模式下,应用程序仍然是32位,而操作系统和驱动程序为64位;在64位模式下,CPU上运行所有的程序全部为64位。

4. 分段式内存模型

在X86-64架构中,传统的分段式内存模型(Segmented Memory Model)被放弃了。程序员在X86-64模式下将使用统一的64位虚拟地址空间,使用实模式和虚拟8086的程序不能在IA-32e扩展模式下运行。

11.4 实验题:Windows同步对象实验

Windows操作系统中有4种同步对象:(1) Mutex对象;(2) Event对象;(3) Semaphore对象;(4) Critical Section对象。thread.asm使用了Mutex对象来同步10个线程对共享变量的更新,对thread.asm进行扩展,使用其他3种对象。
要求:
1. 修改程序界面,允许选取5种同步方式。在“开始计数”之前,选取一种同步方式。选取None时,线程之间不同步,结果如图11-4所示。



图11-4 使用4种同步对象
2. 扩充程序,包含Event对象、Semaphore对象、Critical Section对象的创建,删除,在线程中使用这些对象来保护共享全局变量dwCounter和dwRounds。
3. 5种同步方式下,线程使用同一个函数。在函数内部,根据同步方式执行不同的指令代码。
4. 通过测得的计数速度的差异,比较4种同步对象的效率。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: