《WINDOWS 环境下32位汇编语言程序设计》第12章 多线程

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

12.1 进程和线程

进程和线程在字面上看起来颇为相近,又因为两者是息息相关的,所以往往给初学者造成混淆,其实从英文原文来看,进程(Process)和线程(Thread)是完全不同的。在开始介绍本章中的多线程和下一章中有关进程的内容之前,首先在这里介绍进程和线程的概念,以及它们之间的联系。

进程是正在执行中的应用程序,磁盘上存储的可执行文件只能称之为文件而不能称为进程,内存中正在执行的文件才叫做进程。一个进程是一个执行中的文件使用资源的总和,包括虚拟地址空间、代码、数据、对象句柄、环境变量和执行单元等。当一个应用程序同时被多次执行时,产生的是多个进程,因为虽然它们由同一个文件执行而来,但是它们的地址空间等资源是互相隔离的,这与不同文件在执行的情况是一样的。

进程是不“活泼”的,要使进程中的代码被真正运行起来,它必须拥有在这个环境中运行代码的“执行单元”,这就是线程,线程是操作系统分配处理器时间的基本单位,一个线程可以看成是一个执行单元,它负责执行包含在进程地址空间中的代码。当一个进程被建立的时候,操作系统会自动为它建立一个线程,这个线程从程序指定的入口地址开始执行(对于Win32汇编,就是源代码最后start指定的入口地址),通常把这个线程称为主线程,当主线程执行完最后一句代码的时候,进程也就没有继续存在的理由了,这时操作系统会撤销进程拥有的地址空间和其他资源,对我们来说,这就意味着程序的终止。

在主线程中,程序可以继续建立多个线程来“同时”执行进程地址空间中的代码,这些线程被称为子线程。操作系统为每个线程保存单独的寄存器环境和单独的堆栈,但是它们共享进程的地址空间、对象句柄、代码和数据等其他资源,它们可以执行相同的代码,可以对相同的数据进行操作,也可以使用相同的句柄。读者可以把一个进程中的多个线程看成是进程范围内的“多任务”。

进程和线程的关系可以看做是“容器”和“内容物”的关系,进程是线程的容器,线程总是在某个进程的环境中被创建,它不可以脱离进程而单独存在,而且线程的整个生命周期都存在于进程中,如果进程被结束,其中的线程也就自然结束了。

系统中可以同时存在多个进程,每个进程中同时又可以有多个线程在执行,为了使所有进程中的线程都能够“同时”运行,操作系统为每个线程轮流分配时间片,当轮到一个线程执行的时候,系统将线程的寄存器值恢复回去并开始执行,当时间片结束的时候,系统打断线程的执行,将线程当前的寄存器环境保存下来并切换到另一个线程中去执行,如此循环。当切换到的线程和上一个时间片的线程并不属于同一个进程的时候,操作系统同时切换物理内存到线性地址空间的映射关系,这样线程存取的就是自己所属的进程中的代码和数据。

对于单处理器的计算机来说,不同线程实际上是在轮流使用同一个处理器,一个程序的运行速度并不会因为建立多个线程而加快,因为线程多了以后,每个线程等待的时间也就越长,但是对安装了多个处理器的计算机来说,操作系统可以将不同的线程安排到不同的处理器中去执行,这样,一个进程中的多个线程就会真正获得多个时间片而加快整个进程的运行速度。当然这个过程还需要操作系统的支持。Windows 9x系统不支持多处理器,即使系统中安装有多个处理器,所有线程还是被安排在同一个处理器上运行,其他的处理器则处于空闲状态。Windows NT系统支持多处理器。

虽然,大部分的个人计算机是单处理器的计算机,在应用程序中使用多线程并不能提高程序的运行速度,但多线程编程的出发点并不仅仅是为了使用多处理器,更多的是用来解决一些实际问题。在本章接下来的篇幅中,通过一个典型的“问题程序”来引出多线程编程的内容。

12.2 多线程编程

12.2.1 一个单线程的“问题程序”

来看一个“问题程序”,假设编写一个计数程序,程序的要求如下:

● 界面如图12.1所示,用对话框做主界面,对话框中放置“计数”按钮和“暂停/恢复”按钮,并有一个编辑框用来显示计数结果。

                                                     图12.1 一个有问题的计数程序

● 开始计数之前,“暂停/恢复”按钮处于灰化状态,当开始计数后,该按钮被激活,用户按动一次这个按钮,则计数暂停,再一次按动,则继续计数。

● 开始计数后,“计数”按钮上的文字将被改为“停止计数”,如果按下“停止计数”按钮,程序将恢复初始状态——第一个按钮变回“计数”按钮,同时灰化“暂停/恢复”按钮。

程序的代码在所附光盘的Chapter12\Counter目录中,里面的Counter.rc文件定义了如图12.1所示的对话框,其代码如下:

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include     <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN               1000
#define DLG_MAIN               1000
#define IDC_COUNTER            1001
#define IDC_PAUSE              1002
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN     ICON               "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 227, 187, 129, 48
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "“问题程序” ——计数器"
FONT 9, "宋体"
{
LTEXT "计数值:", -1, 10, 10, 34, 8
EDITTEXT IDC_COUNTER, 47, 8, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP
PUSHBUTTON "计数", IDOK, 8, 27, 50, 14
PUSHBUTTON "暂停/恢复", IDC_PAUSE, 68, 27, 50, 14, WS_DISABLED | WS_TABSTOP
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

汇编源文件写起来似乎很简单,只要在WM_COMMAND消息中对“计数”和“暂停/恢复”按钮的动作进行处理就可以了。在“计数”按钮中,可以调用一个计数子程序不停地进行加法运算并将结果显示出来,为了能够随时停止或暂停,可以设置一个标志位,按动“停止计数”或者“暂停/恢复”按钮时设置不同的标志,计数子程序在循环中通过测试这个标志位来决定是否暂停或退出。按照这个思路,程序可以写成目录中Counter.asm所示的样子:

; Counter.asm
; 一个不合适的程序,在窗口过程中运行长时间工作代码
; 需要改成多线程方式才能正常工作。
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Counter.asm
; rc Counter.rc
; Link  /subsystem:windows Counter.obj Counter.res
.386
.model flat,stdcall 
option casemap:none 

; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

; equ 等值定义
ICO_MAIN 	equ 1000 
DLG_MAIN 	equ 1000
IDC_COUNTER equ 1001 
IDC_PAUSE 	equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?
hWinPause 	dword ?

dwOption 	dword ?
F_PAUSE 	equ 0001h 
F_STOP 		equ 0002h 
F_COUNTING  equ 0004h

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc 
	or dwOption, F_COUNTING 
	and dwOption, not (F_STOP or F_PAUSE)
	invoke SetWindowText, hWinCount, addr szStop 
	invoke EnableWindow, hWinPause, TRUE 
	
	xor ebx, ebx 
	.while !(dwOption & F_STOP)
		.if !(dwOption & F_PAUSE)
			inc ebx 
			invoke SetDlgItemInt, hWinMain, IDC_COUNTER, ebx, FALSE 
		.endif 
	.endw 
	invoke SetWindowText, hWinCount, addr szStart 
	invoke EnableWindow, hWinPause, FALSE 
	and dwOption, not (F_COUNTING or F_STOP or F_PAUSE)
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwOption & F_COUNTING 
				or dwOption, F_STOP 
			.else 
				call _Counter 
			.endif 
		.elseif ax == IDC_PAUSE 
			xor dwOption, F_PAUSE 
		.endif 
	.elseif eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke GetDlgItem, hWnd, IDC_PAUSE 
		mov hWinPause, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 		

这个程序很简单:当用户按下“计数”按钮的时候,WM_COMMAND消息处理代码调用_Counter子程序进行计数,子程序会将IDOK按钮上的文字通过SetWindowText函数改为“停止计数”,并且使用EnableWindow函数激活“暂停/恢复”按钮,然后进入计数循环。

在循环中,程序通过dwOption变量中的第1位(预定义为F_STOP)来判断是否停止,通过第0位(预定义为F_PAUSE)来决定是否暂停计数。这些标志位的状态以后会在按下“停止计数”或“暂停/恢复”按钮时在WM_COMMAND消息中设置。

粗看起来,程序天衣无缝,现在运行一下看看——“计数”按钮被正确地改为“停止计数”,“暂停/恢复”按钮也正确地被激活了,但是接下来就不对了,编辑框中并没有显示计数值,更糟糕的是,接下来所有的按钮都无法按动,对话框也无法移动和关闭,总之,程序停止了响应,现在能结束它的唯一办法是通过任务管理器强制结束!

为什么会这样呢?这是因为主线程自从开始进入计数循环以后,就一直在那里“埋头苦干”,忙于计数工作,以至于把WM_COMMAND消息的处理抛到脑后去了,WM_COMMAND消息没有返回,对话框内部的消息循环就停留在DispatchMessage函数里面,以至于消息队列中的后续消息堆积在那里无法处理,这样,不管用户按动“停止计数”按钮也好,移动对话框也好,这些动作虽然会被Windows检测到并转换成相应的消息放入消息循环中去,但是这些消息堆积在那里无法处理,所以就看不到对话框有任何的响应。

程序进入了一个怪圈:停止或暂停循环的条件是设置标志位,标志位是在按动“停止计数”或“暂停/恢复”按钮的WM_COMMAND消息中设置的,而WM_COMMAND消息被堆积在消息队列中无法处理,结果标志位永远不可能被设置,程序也就永远无法动弹了。虽然在程序一动不动的背后计数工作还在进行,显示计数值的SetDlgItemInt函数也不停地被调用,但是刷新对话框的WM_PAINT消息也同样没有被处理,所以编辑框中的计数值也无法被显示出来。

这个“问题程序”是Win32编程中“1/10秒规则”的一个极端例子,1/10秒规则指窗口过程处理任何一条消息的时间都不应该超过1/10秒,否则会因为消息的堆积造成程序对用户动作的响应变得迟缓。如果一条消息的处理时间超过1/10秒,那么就最好采取别的方法来完成,第4章中介绍的在消息循环中使用PeekMessage来获取空闲时间的方法就是一种,另一种方法是使用定时器在指定的时间间隔中每次完成一小部分工作,但对于这两种方法,程序必须将一个长时间的工作划分成多个小的部分,每部分的操作时间应该少于1/10秒。

显然,这两种方法也不是很好的办法,因为在不同主频的计算机中,1/10秒时间内可以处理的工作量是不同的,如果按照300 MHz处理器设计每小部分工作的工作量,那么到1GHz处理器上运行时,空出来的时间就被浪费了。实际上,解决1/10秒问题的最好办法就是使用多线程编程技术,程序可以建立一个新的线程来完成时间可能超过1/10秒的工作。

12.2.2 多线程的解决方法

1.改进后的程序

对于这个“问题程序”,如果让_Counter子程序在一个新的线程中执行,那么在WM_COMMAND消息的处理中,需要做的工作就仅是启动一个新的线程而已,线程启动后,窗口过程就可以马上返回,消息队列中的消息就可以继续得到处理了。与此同时,_Counter子程序则会在另一个线程中继续运行。

说得多不如做得多,现在用多线程的方法来改进前面的Counter程序,修改后的源代码在所附光盘的Chapter12\Thread目录中,其中Counter.rc文件的内容保持不变。汇编源程序Counter.asm则改为如下代码:

; Counter.asm ------  使用多线程方式来处理长时间运行的计数子程序
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Counter.asm
; rc Counter.rc
; Link  /subsystem:windows Counter.obj Counter.res
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
 
; equ 等值定义
ICO_MAIN 	equ 1000 
DLG_MAIN 	equ 1000 
IDC_COUNTER equ 1001 
IDC_PAUSE 	equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?
hWinPause 	dword ?
dwOption 	dword ?
F_PAUSE 	equ 0001h 
F_STOP 		equ 0002h 
F_COUNTING 	equ 0004h 
 
.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
		 
; 代码段
.code 
_Counter proc uses ebx esi edi _lParam 
	or dwOption, F_COUNTING 
	and dwOption, not (F_STOP or F_PAUSE)
	invoke SetWindowText, hWinCount, addr szStop 
	invoke EnableWindow, hWinPause, TRUE 
	
	xor ebx, ebx 
	.while !(dwOption & F_STOP)
		.if !(dwOption & F_PAUSE) 
			inc ebx 
			invoke SetDlgItemInt, hWinMain, IDC_COUNTER, ebx, FALSE 
		.endif 
	.endw 
	
	invoke SetWindowText, hWinCount, addr szStart 
	invoke EnableWindow, hWinPause, FALSE 
	and dwOption, not (F_COUNTING or F_STOP or F_PAUSE)
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwOption & F_COUNTING 
				or dwOption, F_STOP 
			.else 
				invoke CreateThread, NULL, 0, offset _Counter, NULL, \
						NULL, addr @dwThreadID 
				invoke CloseHandle, eax 
			.endif 
		.elseif ax == IDC_PAUSE 
			xor dwOption, F_PAUSE 
		.endif 
	.elseif eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke GetDlgItem, hWnd, IDC_PAUSE 
		mov hWinPause, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0 
main endp 
end main 
 

运行结果:

将修改后的源程序与修改前的对比一下,可以发现不同的只有两个地方:第一是处理“计数”按钮的WM_COMMAND消息中,调用_Counter子程序的指令变成了对CreateThread函数的调用,这个函数就是用来创建新线程的函数;第二是_Counter子程序的定义有点不同,这是因为用做线程入口的线程函数必须按照规定的格式定义。这两个不同点其实也可以归结为一个,因为第二个不同点实际上是对第一个不同点的配合。

是不是这样修改后程序就正常工作了呢?可以来验证一下:运行程序,按下“计数”按钮,这次在“计数”按钮被改为“停止计数”,“暂停/恢复”按钮被激活的同时,计数值可以在编辑框中显示出来了,而且在计数的过程中,可以移动程序位置、关闭程序、按动各个按钮——总之,计数线程和主线程在同时工作了。

接下来参考这个程序来探讨多线程程序的结构。

2.多线程程序的结构

Windows中存在很多类型的对象:如窗口类、窗口、文件、菜单、图标、光标和钩子等,当一个线程创建某个对象的时候,这个对象归线程所属的进程所拥有,进程中的其他线程也可以使用它们,比如,可以在主线程中打开一个文件,然后在另一个子线程中读写这个文件。

大部分类型的对象不属于创建它的线程,而是属于进程,这表现在创建对象的线程结束时,如果线程不去主动删除这些对象,系统不会自动删除它们,只有当整个进程结束时对象还没有被删除,系统才会自动删除它们。但是窗口和钩子这两种对象比较特殊,它们首先是由创建窗口和安装钩子的线程所拥有的,如果一个线程创建一个窗口或安装一个钩子,然后线程结束,那么系统会自动摧毁窗口或卸载钩子。

进程中的消息队列则与线程和窗口都是相关的,如果在一个线程中创建了一个窗口,那么Windows就会单独给这个线程分配一个消息队列,为了让这个窗口工作正常,线程中就必须存在一个消息循环来派送消息,这就是主线程中只有当创建窗口时才需要消息循环代码,不创建窗口的程序(如控制台程序)就不需要消息循环的原因。这也就意味着如果窗口是在子线程中创建的,主线程中的消息循环根本不会获得这个窗口的消息,子线程必须自己设置一个消息循环。当使用SendMessage或者PostMessage函数向一个窗口发送消息的时候,系统会先判别窗口是被哪个线程创建的,然后把消息派送到正确线程的消息队列中。

整理一下思路:如果在一个线程中创建窗口,就必须设置消息循环,有了消息循环,就必须遵循1/10秒规则,这就意味着这个线程不该用来处理长时间的工作。而在一个程序中为不同的线程设置多个消息循环,不但使代码复杂化,而且会产生诸多的其他问题,所以在多线程程序中,规划好程序的结构是很重要的。

规划多线程程序的原则是:首先,处理用户界面(指拥有窗口和需要处理窗口消息)的线程不该处理1/10秒以上的工作;其次,处理长时间工作的线程不该拥有用户界面。根据这个规则,我们大致可以把线程分成两大类:

● 处理用户界面的线程——这些线程创建窗口并设置消息循环来负责处理窗口消息,一个进程中并不需要太多这种线程,一般让主线程负责这个工作就可以了。

● 工作线程——该类线程不处理窗口界面,当然也就不用处理消息了,它一般在后台运行,干一些繁重的需要长时间运行的粗活。

一般来说,处理用户界面的线程交给主线程来做就可以了。如果主线程中接到一个用户指令,完成这个指令可能需要比较长的时间,主线程可以建立一个工作线程来完成这个工作,并负责指挥这个工作线程。这与我们日常生活中的许多例子是很像的,比如,公司的经理就好比是用户界面线程,他负责和外界沟通、谈判业务、对董事会(指对着屏幕单击鼠标的用户)汇报,同时负责将具体的工作分配给职能部门(也就是工作线程)做,如果让经理具体地去做每一件事情,下车间去包装产品或开着卡车外出拉原料,那么他就无法管理好这个公司了。

在多线程版本的Counter.asm例子中,使用的就是这样的程序结构:主线程用来维护界面,接收用户的输入动作并安排相应的操作,工作线程则用来进行计数操作。

3.线程之间的通信

主线程在创建工作线程的时候,可以通过参数给工作线程传递初始化数据,当工作线程开始运行后,还需要通过通信机制来控制工作线程,就像经理虽然不用亲自干活,也需要随时了解和控制情况一样;同样,工作线程有时候也需要将一些情况主动通知主线程。

线程之间的通信可以归纳为3种方法。

使用全局变量传递数据是最常用的方法,如例子中主线程通过设置全局变量dwOption中的数据位来通知工作线程,工作线程随时检查这个变量并根据要求做相应的动作;反过来,工作线程也通过设置dwOption的第2位(预定义为F_COUNTING)来控制主线程中对IDOK按钮的动作,如果F_COUNTING被置位,表示线程在运行中,这时IDOK按钮被定义为“停止计数”按钮,否则IDOK按钮被定义为“计数”按钮。使用全局变量传递数据的缺点是当多个工作线程使用同一个全局变量时,可能会引起同步问题,在12.4节中会探讨这个问题。

第2种方法是通过发送消息来通信,如工作线程工作结束时,可以通过向主线程发送自定义的WM_XXX消息来通知主线程,这样主线程就不需要随时去检查工作线程是否结束。只要在窗口过程中处理WM_XXX消息就可以了。这种方法的缺点是无法向工作线程发送消息,因为工作线程中一般并没有消息队列,所以这种方法仅用在工作线程向主线程传递消息的应用中。

如果线程之间传递的不是数据而是代表状态的布尔值,也可以使用第3种方法,即使用事件对象来通信,相关内容会在12.3节中详细介绍。

12.2.3 与线程有关的函数

1.创建线程

创建一个线程可以使用CreateThread函数,函数的用法是:

invoke CreateThread,lpThreadAttributes,dwStackSize,lpStartAddress,\
    dwParameter,dwCreationFlags,lpThreadId
.if eax
    mov hThread,eax
.endif

函数使用的参数定义如下:

● lpThreadAttributes——指向一个SECURITY_ATTRIBUTES结构,用来定义线程的安全属性,这个结构在CreateFile函数的介绍中已经涉及过,主要用来指定句柄是否可以继承,如果想让线程使用默认的安全属性,可以将参数设置为NULL。

● dwStackSize——线程的堆栈大小。如果指定为0,那么线程的堆栈大小和主线程使用的大小相同。系统自动在进程的地址空间中为每个新线程分配私有的堆栈空间,这些空间在线程结束的时候自动被系统释放,如果需要的话,堆栈空间会自动增长。

● lpStartAddress——线程开始执行的地址。这个地址是一个规定格式的函数的入口地址,这个函数就被称为“线程函数”。

● dwParameter——传递给线程函数的自定义参数。

● dwCreationFlags——创建标志。如果是0,表示线程被创建后立即开始运行,如果指定CREATE_SUSPENDED标志,表示线程被创建后处于挂起状态,直到使用ResumeThread函数显式地启动线程为止。

● lpThreadId——指向一个双字变量,用来接收函数返回的线程ID。线程ID在系统范围内是唯一的,一些函数需要用到线程ID。

如果线程创建成功,函数返回一个线程句柄,这个句柄可以用在一些控制线程的函数中,如SuspendThread,ResumeThread和TerminateThread等函数,如果线程创建失败,那么函数返回NULL。

当程序调用CreateThread函数时,首先系统为线程建立一个用来管理线程的数据结构,其中包含线程的一些统计信息,如引用计数和退出码等,这个数据结构被称为线程对象;接下来系统将从进程的地址空间中为线程的堆栈分配内存并开始线程的执行。当线程结束时,线程的堆栈被释放,但是线程对象不会马上被释放,系统保留它以便其他线程可以通过它检测线程的有关情况,直到使用CloseHandle函数关闭线程句柄后,线程对象才会被释放。

但是线程对象也可以提前被释放,对于大部分的句柄来说(如文件句柄hFile,文件寻找句柄hFindFile等),使用CloseHandle函数关闭句柄意味着整个对象被释放,但对于线程句柄来说,关闭它仅释放线程的统计信息,并不会终止线程的执行,所以如果不再需要使用线程句柄的话,在调用CreateThread后马上就可以将它关闭掉,线程的执行并不会受影响。

2.线程函数

如果创建线程时没有指定CREATE_SUSPENDED标志,当CreateThread函数返回时,lpStartAddress参数指向的线程函数就已经开始运行了。线程函数包含所有需要在线程中执行的代码,它有一个输入参数,线程函数的一般书写格式是:

_ProcThread proc uses ebx esi edi lParam
    local   局部变量
    ...
    mov      eax,返回码
    ret
_ProcThread endp

读者可以自由定义函数的名称,只要在使用CreateThread函数时将lpStartAddress参数指向函数的入口地址就可以了,lParam参数传递过来的就是调用CreateThread函数时使用的dwParameter参数。

向线程函数传递参数的时候,读者可能会觉得一个lParam参数不太够用,如果需要传递多个参数该怎么办呢?其实这不是问题,因为子线程和主线程使用同一个地址空间,主线程可以通过全局变量来传递参数。

有时候也可能遇到这种情况:进程中存在多个子线程,这些子线程的线程函数使用同一个子程序,如果对这些子线程使用同样的全局变量传递参数,难免会引起冲突。这时可以为每个子线程分配一个存放参数的内存块,主线程通过lParam参数把内存块的指针传递给子线程,子线程通过这个指针存取内存块中的内容就可以了,不过在子线程结束的时候不要忘了释放内存块。

3.终止线程

线程从线程函数的第一句代码开始执行,直到线程被终止为止。当线程被正常终止时,系统会进行下面的操作:

● 线程使用的堆栈被释放。

● 系统将线程对象中的退出代码设置为线程的退出码。

● 系统将递减线程对象的使用计数。

由于线程结束后的退出码可以被其他线程用GetExitCodeThread函数检测到,所以可以当做自定义的返回值来表示线程执行的结果。终止一个线程的执行有4种方法。

第1种方法是线程函数的自然退出,当函数执行到一句ret指令返回时,Windows将终止线程的执行,这时放在eax中的返回值就是线程的退出码。一般建议使用这种方法终止一个线程的执行。

第2种方法是使用ExitThread函数来终止线程:

invoke   ExitThread,dwExitCode

ExitThread函数只能用于终止当前线程,它并不能用于在一个线程中终止另外一个线程,和ExitProcess函数一样,ExitThread函数不会有返回的时候。dwExitCode参数指定为线程的退出码。使用ExitThread函数和使用ret指令终止线程的效果是一样的,但显然不如使用ret指令来得简洁和方便。

第3种方法是使用TerminateThread函数,这个函数可以用来在一个线程中强制终止另一个线程的执行:

invoke TerminateThread,hThread,dwExitCode

hThread参数指定需要终止的线程句柄,dwExitCode将用做被终止线程的退出码。如果函数执行成功,返回值是非0值,否则函数返回0,但是TerminateThread函数是一个异步执行的函数,即使函数返回非0值,也并不代表目标线程已经终止,可能终止的过程还要延续一段时间,如果必须确认线程已经真正结束的话,可以使用GetExitCodeThread函数来检测。

TerminateThread函数是一个被强烈建议避免使用的函数,因为一旦执行这个函数,程序无法预测目标线程会在何处被终止,其结果就是目标线程可能根本没有机会来做清除工作。读者可以尝试在Counter.asm例子中使用TerminateThread函数来终止_Counter线程的执行。可以发现计数线程是停止了,但是“停止计数”按钮并不会恢复为“计数”按钮,“暂停/恢复”按钮也不会被灰化。因为计数线程平时在循环中执行,被强制终止的时候必然还在循环体内,这样下面的扫尾代码将没有机会执行,其结果当然如此了:

invoke   SetWindowText,hWinCount,addr szStart
invoke   EnableWindow,hWinPause,FALSE
and      dwOption,not (F_COUNTING or F_STOP or F_PAUSE)

TerminateThread函数引发的问题可能还有很多,如线程中打开的文件和申请的内存等都不会被释放,更危险的是,如果线程刚好在调用Kernel32.dll中的系统函数时被终止,可能会引起Kernel32的状态处于不正确的状态(当然只是线程所属进程的Kernel32状态而不是系统范围的状态)。另外,当使用TerminateThread函数终止线程的时候,系统不会释放线程使用的堆栈。所以建议读者在编程中的时候尽量让线程自己退出,如果主线程要求某个线程结束,可以通过各种方法通知线程,线程收到通知在做扫尾工作后自行退出。只有在迫不得已的情况下,才能使用TerminateThread函数去终止一个线程。

第4种方法就是使用ExitProcess函数结束进程,这时系统会自动结束进程中所有线程的运行。在以前演示的所有的单线程程序中,并不显式地结束主线程的运行,而总是用直接结束进程的方法让主线程自然结束。在多线程的程序中,用这种方法结束线程相当于对每个线程使用TerminateThread函数,所以也应当避免这种情况(用这种方法结束主线程的运行并不是问题,因为在这之前可以预测到线程的结束并进行扫尾工作)。

当一个线程终止时,Windows释放执行线程所需的各种资源,如堆栈与寄存器环境等,并且不再继续分配时间片调用线程中的代码,但线程对象并不马上被释放,因为以后其他线程可能还需要用GetExitCodeThread函数检测线程的退出码。线程对象一直保存到使用CloseHandle函数关闭线程句柄为止。

4.其他相关函数

除了上面介绍的一些函数,读者还可以通过其他的相关函数对线程进行控制。下面简单介绍SuspendThread,ResumeThread和GetExitCodeThread函数的用法。

一个线程可以被挂起(暂停),也可以在挂起后被恢复执行。当使用CreateThread函数创建线程的时候,如果在dwCreationFlags参数中指定CREATE_SUSPENDED标志,线程创建后并不马上开始执行,而是处于被挂起的状态,直到使用ResumeThread函数启动它为止。除了在创建的时候直接让线程处于挂起状态,也可以使用SuspendThread函数将运行中的线程挂起:

invoke   SuspendThread,hThread

该函数的唯一参数是需要挂起的线程句柄。系统为每个线程维护一个暂停计数器,SuspendThread函数将导致线程的暂停计数增加,当一个线程的暂停计数大于0的时候,系统就不会给线程安排时间片,这就相当于将线程挂起,如果函数执行成功,返回值是线程原来的暂停计数值,当函数执行失败时,返回值是-1。如果创建线程的时候使用CREATE_SUSPENDED标志,那么线程的暂停计数值一开始就是1。

要将挂起的线程恢复到执行状态,可以使用ResumeThread函数:

invoke   ResumeThread,hThread

该函数减少线程的暂停计数,当计数值减到0的时候,线程被恢复运行,所以函数被调用后线程是否被恢复运行还要看原来的暂停计数值是多少,如果多次调用SuspendThread函数导致暂停计数值远远大于1的话,就必须多次调用ResumeThread后线程才能被恢复运行。ResumeThread的返回值定义和SuspendThread函数的定义是一样的。

一个线程可以将别的线程挂起,也可以将自己挂起,但是将自己挂起后,显然不可能再由自己来恢复运行,因为这时线程自己不可能再运行ResumeThread函数了,在这种情况下,必须由其他线程来进行恢复操作。

在例子程序中,也可以将通过检测标志位来“暂停/恢复”的功能改为使用挂起/恢复计数线程的办法来实现。

GetExitCodeThread函数用来获取线程的退出码,同时也可以用来检测线程是否已经结束。函数的用法是:

invoke GetExitCodeThread,hThread,lpExitCode

其中hThread参数指定需要获取的线程句柄,lpExitCode指向一个双字变量,用来接收函数返回的退出信息,如果函数执行成功,返回非0值,并且将退出码返回到lpExitCode指向的变量中,如果执行失败,函数返回0。

当一个线程没有结束的时候,退出信息中返回的是STILL_ACTIVE,如果线程已经结束,那么变量中返回的就是线程的退出码,通过检查退出信息是否为STILL_ACTIVE就可以得知线程是否已经结束。

12.3 使用事件对象控制线程

12.2.2节中经过改进的多线程版的Counter程序运行起来一切正常,但是不知道读者有没有发现一个小缺点——在CPU时间占用上的小缺点。

如果程序在Windows NT系列操作系统中运行,就可以从任务管理器中发现这个问题(可以通过按下Ctrl+Alt+Del键调出任务管理器程序),如图12.2所示,当计数正在进行的时候,任务管理器显示Counter.exe程序的CPU占用率为96%,这没有什么奇怪,因为当前只有这一个程序在瞎忙活,并没有其他大运算量的程序,所以Counter.exe程序占用了绝大部分的CPU时间。

                                                     图12.2 Counter程序的CPU占用率

现在按下“暂停/恢复”按钮将计数暂停,就可以看出问题来了——即使计数暂停了,但是程序的CPU占用率还是保持不变,根本没有降下来,这是为什么呢?其实不难解释,在_Counter子程序中使用下面的语句来检测是否暂停:

.if !(dwOption & F_PAUSE)
      inc ebx
      invoke SetDlgItemInt,hWinMain,IDC_COUNTER,ebx,FALSE
.endif

当计数暂停的时候,dwOption的F_PAUSE位被设置,这时程序跳过了中间的inc ebx指令和SetDlgItemInt函数,但是为了随时能够响应用户恢复计数的动作,程序不得不循环检测dwOption变量,以至于虽然没有做任何有用功,但还是把所有的CPU时间都花在了检测标志上面。

对于这样一个小程序来说,效率不是主要的问题,但如果在一个大型的拥有很多线程的程序中,这就会严重影响效率。对于这种问题,最彻底的解决方法就是让操作系统来决定是否继续执行程序,如果操作系统了解线程什么时候需要等待,什么时候需要执行的话,它就可以仅在线程需要执行的时候安排时间片,在等待的时候干脆连时间片都不用分配,这样就不会在检测标志上浪费时间了

按照这个思路,使用SuspendThread和ResumeThread函数来挂起和恢复线程是一个可行的办法,主线程不必通过设置标志位来通知工作线程进入等待状态,而是直接使用SuspendThread函数将工作线程挂起就可以了。使用这种方法的好处是可以解决CPU利用率的问题,因为操作系统不会给挂起的线程分配时间片,缺点就是无法精确地控制线程,因为主线程不知道工作线程会在哪里被暂停,暂停点可能会在inc ebx指令上,也有可能在测试dwOption的指令中,甚至在执行SetDlgItemInt函数的系统内核中。如果要求工作线程必须在完成整个循环体代码的情况下才能暂停的话,就无法使用这种方法,这时必须在循环体的头部进行条件测试。

难道除了不断地测试暂停标志就没有其他方法了吗?当然不是,下面介绍的事件对象就可以用来解决这个问题。

12.3.1 事件

Windows中可以创建很多种类的对象,如文件、窗口和内存等对象都是看得见摸得着的实体,事件(Event)也是一种对象,但事件对象比较抽象,我们可以把它看成是一个设置在Windows内部的标志,它的状态设置和测试工作由Windows来完成,Windows可以将这些标志的设置和测试工作和线程调度等工作在内部结合起来,这样效率就要高得多。

事件可以有两种状态:“置位的”和“复位的”。如果想使用事件对象,需要首先使用CreateEvent函数去创建它,就像在程序中为自己的标志变量分配内存一样:

invoke CreateEvent,lpEventAttributes,bManualReset,bInitialState,lpName
.if eax
    mov hEvent,eax
.endif

函数的参数定义如下:

● lpEventAttributes参数指向一个SECURITY_ATTRIBUTES结构,用来定义事件对象的安全属性,如果事件对象的句柄不需要被继承,可以在这里指定NULL。

● bManualReset参数指定事件对象是否需要手动复位,如果指定TRUE,对事件对象状态的复位工作必须使用ResetEvent函数手动完成。指定FALSE的话,当测试事件的函数返回时(返回原因可能是超时,也可能是对象状态被置位引起),对象的状态会自动被复位。

● bInitialState参数指定事件对象创建时的初始状态,TRUE表示初始状态是置位状态,FALSE表示初始状态是复位状态。

● lpName指向一个以0结尾的字符串,用来指定事件对象的名称,和内存共享文件一样,为事件对象命名是为了在其他地方使用OpenEvent函数获取事件对象的句柄。

如果不需要命名,那么可以在这里使用NULL。

如果函数执行成功,函数的返回值是事件的句柄,如果失败,则返回0。

当一个事件被建立后,程序就可以通过SetEvent和ResetEvent函数来设置事件的状态,就像我们使用or或and指令将程序中的标志变量置位或复位一样:

invoke  SetEvent,hEvent          ;将事件的状态设为“置位”
invoke  ResetEvent,hEvent        ;将事件的状态设为“复位”

参数hEvent就是CreateEvent函数返回的事件句柄。当不再需要事件对象的时候,可以使用CloseHandle函数将它释放掉。

12.3.2 等待事件

就像用测试指令来测试标志一样,如果将事件看成是“标志”的话,就需要有函数来实现测试功能,WaitForSingleObject就是这样的函数,注意:函数的名称包含Wait(“等待”)一词而不是“测试”,如果函数仅可以用来测试事件的状态的话,事件对象就失去了使用的初衷,因为这样的话,在线程中循环测试标志的情况又会重演了。

WaitForSingleObject函数的用法是:

invoke WaitForSingleObject,hHandle,dwMilliseconds

WaitForSingleObject函数可以测试的不仅是事件对象,它也可以用来测试线程和进程等对象的状态,hHandle参数用来指定为等待的对象句柄,dwMilliseconds参数指定以ms为单位的超时时间,当以下两种情况中的任意一种发生的时候,函数就返回:

● 测试对象的状态变为置位状态。

● 到了dwMilliseconds指定的超时时间。

如果dwMilliseconds参数指定为0的话,WaitForSingleObject在测试对象的状态后马上返回,如果需要函数无限期等待直到对象的状态变为“置位”为止的话,可以在该参数中使用INFINITE预定义值。

如果函数执行失败,返回值为WAIT_FAILED。如果函数执行成功,返回值代表函数返回的原因,当返回值是WAIT_OBJECT_0时,表示返回原因是对象的状态被置位,返回值是WAIT_TIMEOUT的时候表示返回原因是超时。

函数可以测试的对象有多种,不同的对象对状态的定义是不同的,下面列出了部分函数支持的对象对状态的定义:

● 控制台输入(Console input)——如果用户的输入使控制台的输入缓冲区不为空的时候,控制台对象的状态为“置位”,当输入缓冲区空的时候,状态变为“复位”。

● 事件对象(Event)——对事件对象调用SetEvent函数后,状态为“置位”,对事件对象调用ResetEvent函数后,状态为“复位”。

● 进程对象(Process)——如果进程结束,状态为“置位”。

● 线程对象(Thread)——如果线程结束,状态为“置位”。

可以看到,WaitForSingleObject函数也可以很方便地用来等待线程结束,这样当程序必须等待某个线程结束的时候,就不必用一个循环不停调用GetExitCodeThread函数,然后通过检测返回值是否还是STILL_ACTIVE来判断了。

WaitForSingleObject函数仅可以测试一个对象,在实际的应用中,还常常会遇到需要同时测试多个对象的情况,这时可以使用另外一个函数:WaitForMultipleObjects。这个函数的用法是:

invoke WaitForMultipleObjects,dwCount,lpHandles,bWaitAll,dwMilliseconds

lpHandles指向一组对象句柄变量,对象句柄的数量由dwCount参数指定,函数将同时测试这些对象句柄的状态。

bWaitAll参数用来定义测试的逻辑。如果指定为TRUE,函数仅在所有对象的状态都变成“置位”时才返回(相当于执行and操作)。如果指定为FALSE,任意一个对象的状态变成“置位”时,函数就会返回(相当于执行or操作)。

函数的其他用法,如dwMilliseconds参数以及返回值的定义和WaitForSingleObject中的定义都是相同的。

12.3.3 进一步改进计数程序

现在让我们进一步改进前面的计数程序,用事件对象代替暂停标志,用WaitForSingleObject函数代替测试暂停标志的语句,这样就可以解决CPU占用率的问题。改进的步骤如下:

● 在程序初始化的时候用CreateEvent函数建立事件对象,以便当做暂停标志使用。

● 当计数线程开始的时候,使用SetEvent函数将事件的初始状态设置为“置位”。

● 计数循环中使用WaitForSingleObject函数测试事件状态,当不需要暂停的时候,由于事件的状态为“置位”,函数会马上返回,循环继续执行。主线程中通过使用ResetEvent函数将事件复位来暂停线程,因为这时进入WaitForSingleObject函数后不会返回,直到主线程中继续使用SetEvent函数将事件置位为止。

● 退出程序的时候用CloseHandle函数删除事件对象。

修改后的代码在所附光盘的Chapter12\Event目录中,Counter.rc文件并没有改动。改动后的Counter.asm文件如下:

; Counter.asm ------- 使用事件对象来作为多个线程之间的“信号灯”
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Counter.asm
; rc Counter.rc
; Link  /subsystem:windows Counter.obj Counter.res
.386
.model flat,stdcall 
option casemap:none 

; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
 
; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000 
IDC_COUNTER equ 1001 
IDC_PAUSE 	equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?
hWinPause 	dword ?
hEvent 		dword ?
dwOption 	dword ?
F_PAUSE 	equ 0001h 
F_STOP 		equ 0002h  
F_COUNTING 	equ 0004h 

.const 
szStop	 	byte '停止计数',0
szStart		byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	or dwOption, F_COUNTING 
	and dwOption, not (F_STOP or F_PAUSE)
	invoke SetEvent, hEvent 
	invoke SetWindowText, hWinCount, addr szStop 
	invoke EnableWindow, hWinPause, TRUE 
	
	xor ebx, ebx 
	.while !(dwOption & F_STOP)
		inc ebx 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER, ebx, FALSE 
		invoke WaitForSingleObject, hEvent, INFINITE 
	.endw 
	
	invoke SetWindowText, hWinCount, addr szStart 
	invoke EnableWindow, hWinPause, FALSE 
	and dwOption, not (F_COUNTING or F_STOP or F_PAUSE)
	ret
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwOption & F_COUNTING 
				invoke SetEvent, hEvent 
				or dwOption, F_STOP 
			.else 
				invoke CreateThread, NULL, 0, offset _Counter, NULL, \
						NULL, addr @dwThreadID 
				invoke CloseHandle, eax 
			.endif 
		.elseif ax == IDC_PAUSE 
			xor dwOption, F_PAUSE 
			.if dwOption & F_PAUSE 
				invoke ResetEvent, hEvent 
			.else 
				invoke SetEvent, hEvent 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		invoke CloseHandle, hEvent 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke GetDlgItem, hWnd, IDC_PAUSE 
		mov hWinPause, eax 
		invoke CreateEvent, NULL, TRUE, FALSE, NULL 
		mov hEvent, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main  

修改以后编译执行,然后打开任务管理器观察程序的CPU占用率就可以发现,当按下“暂停/恢复”按钮暂停计数后,程序的CPU占用率马上下降到接近零,再次按动按钮恢复计数时,CPU占用率会恢复到原来的数值,这说明程序的运行效率得到了很大的提高。

12.4 线程间的同步

12.4.1 产生同步问题的原因

对于多线程程序来说,线程之间的同步永远是个重要的问题。如果多个线程都要存取同样的对象(如存取同样的内存变量或读写同一个文件等),而一个线程操作的结果反过来又会影响另一个线程的运行的时候,同步问题就变得异常重要。

产生同步问题的根源在于线程之间的切换是无法预测的,一个线程无法知道什么时候自己的时间片会结束,也无法知道下一个时间片会被分配给哪个线程。事实上,线程可以在任何地方被Windows打断。读者应该记住的是:唯一可以确定的事实就是线程只能在两条指令之间被打断,因为指令是CPU执行的最小单位,线程不可能在一条指令执行到一半的时候被打断。

对于单线程的程序来说,主线程在单个时间片结束的时候被Windows挂起,然后在轮到下一个时间片的时候继续执行,这中间整个进程的环境不会有任何改变,因为进程中不存在其他线程。但对于多线程的程序来说,在主线程挂起的过程中可能会有子线程被分配了时间片,如果子线程在执行中改变了主线程正在存取的对象,就可能会引发错误的结果。举一个例子来说明,假定往银行账户里存钱的操作步骤有3步:

(1)获取账户的余额。

(2)将用户存入的数额和余额相加,得到新的余额。

(3)将账户中的余额数据更新为新的数值。

现在从两个不同的储蓄所里同时向一个账户存钱,假设原来的余额是10000元,储蓄所A要存入1000元,储蓄所B要存入2000元,如果没有同步机制,就可能发生下面的情况:

① 储蓄所A首先执行第(1)步,获得余额数据10000元,然后进行第(2)步运算得到结果11000元。

② 这时储蓄所B的业务也刚好发生,在储蓄所A计算第(2)步的过程中,储蓄所B执行了第(1)步,由于储蓄所A还没有执行到第(3)步,所以账户余额还没有被更新,储蓄所B得到的余额数据还是10000元。

③ 储蓄所B计算新余额,得到结果12000元。

④ 在储蓄所B计算新余额的过程中,储蓄所A执行了第(3)步,将余额更新为11000元。

⑤ 最后,储蓄所B执行了第(3)步,将自己的计算结果12000元更新到余额数据中。

结果就是储蓄所A的业务实际上是丢失了;另一种情况,假如储蓄所B的动作很快,在上面的第④步骤发生之前,在第③步骤中就将计算结果12000更新到余额数据中了,那么在第④步骤中储蓄所A的计算结果11000就会将12000覆盖,这时的结果就是储蓄所B的业务丢失了,所以同步问题产生的错误结果是很难预测的。

将这个比喻引申到两个线程的同步问题上就表现在:假如线程A将某个内存变量的值取到eax寄存器中,准备在经过运算后将结果写回去,这时被Windows切换到线程B中,线程B在这个时间片中对同一个内存变量进行了修改,当切换回线程A的时候,线程A在上一个时间片中刚取到eax中的数值和内存变量中的值就不同步了,计算结果当然就是错误的。

有人可能会认为出现这种情况的概率是很低的,线程中有这么多条指令要执行,难道偏偏就在程序取完数据还没开始处理的时候被系统打断吗?通过下面的例子就可以发现发生这种事情的可能性有多大。例子程序位于所附光盘的Chapter12\ThreadSynErr目录中,还是用递增计数器的方法来演示同步问题,ThreadSynErr.asm的内容如下:

; ThreadSynErr.asm
; 一个存在同步错误的多线程程序,用来演示同步错误的发生
;---------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ThreadSynErr.asm
; rc ThreadSynErr.rc
; Link  /subsystem:windows ThreadSynErr.obj ThreadSynErr.res
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

; equ 等值定义
ICO_MAIN 	equ 1000 
DLG_MAIN 	equ 1000
IDC_COUNTER1 equ 1001 
IDC_COUNTER2 equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?
dwThreads 	dword ?

dwOption 	dword ?
F_STOP 		equ 0001h 

dwCounter1 	dword ?
dwCounter2 	dword ?

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	inc dwThreads 
	invoke SetWindowText, hWinCount, addr szStop 
	and dwOption, not F_STOP 
	
	.while !(dwOption & F_STOP)
		inc dwCounter1 
		mov eax, dwCounter2 
		inc eax 
		mov dwCounter2, eax 
	.endw 
	dec dwThreads 
	invoke SetWindowText, hWinCount, addr szStart 
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_TIMER 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER1, dwCounter1, FALSE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER2, dwCounter2, FALSE 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwThreads 
				or dwOption, F_STOP 
				invoke KillTimer, hWnd, 1 
			.else 
				mov dwCounter1, 0
				mov dwCounter2, 0 
				xor ebx, ebx 
				.while ebx < 10
					invoke CreateThread, NULL, 0, offset _Counter, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
					inc ebx 
				.endw 
				invoke SetTimer, hWnd, 1, 500, NULL 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		.if !dwThreads 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 
 
;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 
 

目录中的ThreadSynErr.rc文件定义了如图12.3所示的界面。

 图12.3 多线程同步的演示程序

ThreadSynErr.rc文件的代码为:

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include                   <c:/masm32/icnlude/resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN           1000
#define DLG_MAIN           1000
#define IDC_COUNTER1       1001
#define IDC_COUNTER2       1002
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN         ICON      "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 227, 187, 129, 56
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "多线程同步的演示程序"
FONT 9, "宋体"
{
LTEXT "计数器一:", -1, 7, 7, 40, 8
LTEXT "计数器二:", -1, 7, 22, 41, 8
EDITTEXT IDC_COUNTER1, 51, 5, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP
EDITTEXT IDC_COUNTER2, 51, 20, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP
PUSHBUTTON "计数", IDOK, 72, 36, 50, 14
}
    

“问题程序”还是用循环计数的功能来演示,程序中设置了两个全局变量dwCounter1和dwCounter2用做计数器,当按下“计数”按钮时,程序将两个计数器清零,并且用循环语句建立10个线程来同时执行_Counter线程函数:

mov dwCounter1,0
mov dwCounter2,0
xor ebx,ebx
.while ebx <   10
  invoke CreateThread,NULL,0,offset _Counter,NULL,\
            NULL,addr @dwThreadID
  invoke CloseHandle,eax
  inc ebx
.endw
invoke SetTimer,hWnd,1,500,NULL

最后,程序设立一个定时器来定时将计数器值显示到编辑框中。

在线程函数中,使用下面的计算代码:

.while ! (dwOption & F_STOP)
    inc dwCounter1
    mov eax,dwCounter2
    inc eax
    mov dwCounter2,eax
.endw

在这段代码中,递增第一个计数器使用inc指令,由于计算用单条指令完成,所以计数器一不会因为同步问题出错,递增第二个计数器的代码使用了3条指令,首先将原来的计数值取到eax中,递增eax后再写回到变量中,如果不存在多个线程同步的问题,这两种算法的结果是一样的,显示到编辑框中的计数值应该是相等的。

在存在同步问题的情况下,如果线程在mov eax,dwCounter2或者inc eax指令执行后被打断,并且其他线程在这期间修改了dwCounter2的值的话,根据前面的分析,就会有一次计数值被丢失。如果显示到编辑框中的计数值不相等,则证明存在同步的问题,通过比较两个计数值的差值,还可以得知同步问题发生的机会是多少。

好了,大家可能都迫不及待地想看运行结果了吧,结果如图12.3所示,这是程序在笔者的700 MHz的计算机上运行了10秒以后的结果,可以看到,10个线程加起来总共进行了1174582125次计算,计数器二却丢失了1174582125-413019673=761562452次计数,因为同步问题丢失的计数竟然占了65%,可见这绝对不是偶尔发生一次两次的事情,大家可以想象一下,如果有人往一个银行账户中汇款,三笔汇款中丢了两笔,人们会有何感想呢?

12.4.2 各种用于线程间同步的对象

了解了同步问题产生的根源,再提出解决方案是很简单的,这在其他的应用程序中早有体现,如各种多用户版的数据库在操作记录之前都要对记录进行锁定,保证一条记录在同一时刻只能被一个对象操作;Windows中的写文件函数在遇到其他程序正在写入的时候会返回共享错误,而不是不管青红皂白直接写入了事。类似的例子还可以找到很多,归纳起来不外乎一点:就是保证整个存取过程的独占性,也就是当一个线程要进行操作前,需要等待其他操作中的线程结束。

在编程上,独占机制不能简单地用一个标志变量来解决,因为测试标志位和改变标志位的过程仍然可能被其他线程打断,导致有多个线程同时认为标志有效并对数据进行操作。如果有种方法,能够保证标志位的测试和改变过程不被打断就行了。

幸亏Windows提供了多种同步对象供我们使用,仔细回想本章中介绍的事件对象,就可以发现事件对象符合这个特征,CreateEvent时将bManualReset参数设置为FALSE的时候,测试事件的函数返回时,对象的状态会自动被复位,测试和设置状态的过程不会被其他线程打断。除了事件(Event)对象外,临界区(Critical Section)、互斥量(Mutex)和信号灯(Semaphore)等对象都可以用于线程同步。

1.使用事件对象进行线程间同步

现在来看看如何用事件(Event)对象来进行线程间同步,方法很简单:在使用CreateEvent创建事件对象的时候,将bManualReset参数设置为FALSE,然后在需要独占操作的代码前面加上一个WaitForSingleObject函数,后面加上一个SetEvent即可。

由于bManualReset参数为FALSE,这样当某个线程等待到Event后,Event对象的状态马上就变为复位状态,这样其他的线程执行到WaitForSingleObject时就全部处于等待中了,当活动的线程操作完毕后,执行SetEvent函数,Event对象的状态才恢复到置位,这样其他等待中的线程才会有一个能继续操作。

来看看具体实现的代码,代码在所附光盘的Chapter12\ThreadSyn\UseEvent目录中,其中ThreadSyn.rc文件和上例中的ThreadSynErr.rc文件一模一样,而修改后的ThreadSyn.asm文件如下:

; ThreadSyn.asm  ------  使用事件对象进行线程之间的同步
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ThreadSyn.asm
; rc ThreadSyn.rc
; Link  /subsystem:windows ThreadSyn.obj ThreadSyn.res
.386
.model flat, stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
 
; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000
IDC_COUNTER1 equ 1001 
IDC_COUNTER2 equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?

dwCounter1 	dword ?
dwCounter2 	dword ?

dwThreads 	dword ?
hEvent 		dword ?
dwOption 	dword ?
F_STOP 		equ 0001h 

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	inc dwThreads 
	invoke SetWindowText, hWinCount, addr szStop 
	and dwOption, not F_STOP 
	.while !(dwOption & F_STOP)
		invoke WaitForSingleObject, hEvent, INFINITE 
		inc dwCounter1 
		mov eax, dwCounter2 
		inc eax 
		mov dwCounter2, eax 
		invoke SetEvent, hEvent 
	.endw 
	dec dwThreads 
	invoke SetWindowText, hWinCount, addr szStart 
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg,wParam,lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_TIMER 
		invoke WaitForSingleObject, hEvent, INFINITE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER1, dwCounter1, FALSE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER2, dwCounter2, FALSE 
		invoke SetEvent, hEvent 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwThreads 
				or dwOption, F_STOP 
				invoke KillTimer, hWnd, 1 
			.else 
				mov dwCounter1, 0
				mov dwCounter2, 0
				xor ebx, ebx 
				.while ebx < 20 
					invoke CreateThread, NULL, 0, offset _Counter, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
					inc ebx 
				.endw 
				invoke SetTimer, hWnd, 1, 500, NULL 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		.if !dwThreads 
			invoke CloseHandle, hEvent 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke CreateEvent, NULL, FALSE, TRUE, NULL 
		mov hEvent, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

请读者注意代码中的粗体部分,程序在WM_INITDIALOG消息中创建了一个事件对象:

invoke CreateEvent,NULL,FALSE,TRUE,NULL

注意第三个参数是TRUE,这样创建的事件对象的初始状态为置位,保证肯定有一个线程能够通过WaitForSingleObject函数,如果这个参数是FALSE的话,事件对象的初始状态是复位,那么所有的线程都会处于等待状态,程序也就无法正常工作了。

另外,除了在线程的计数代码前后有WaitForSingleObject和SetEvent语句,在显示计数结果的WM_TIMER消息中,SetDlgItemInt函数的前后也有同样语句,这是为了防止在两句SetDlgItemInt之间其他线程改变计数值造成显示结果的不统一。

将代码经过编译链接后再运行,就可以发现两个计数器的值永远保持统一了!读者还可以发现一个现象:在相同的时间内,正确同步的程序的计数值要远远小于同步不正确的程序,比如在笔者的计算机上,同样是运行10秒,这个程序只能计数2249473次,与前述例子的1174582125次相比,前者不及后者的2‰,这说明花在等待上的时间实在是太多了,但是这是多线程程序为了保持数据同步所必须付出的代价。

事件对象不仅可以用于一个进程中的多线程同步,还可以用于多个进程中的线程同步,在CreateEvent的时候,如果将最后一个参数lpName指向一个名字字符串,则创建的事件对象是命名的,这时在其他进程中可以使用OpenEvent函数根据名称打开该事件对象进行使用。

2.使用临界区对象进行线程间同步

临界区对象(Critical Section)是定义在数据段中的一个CRITICAL_SECTION结构,在使用时,结构的具体字段不必关心,也不应该关心,因为它的维护和测试工作都是由Windows来完成的,读者只需把它想像成一个标志就可以了,结构应当定义成全局变量,因为在各线程中都要测试它。

定义了CRITICAL_SECTION结构后,必须首先对它进行初始化,这个步骤类似于使用事件对象时的CreateEvent操作:

invoke InitializeCriticalSection, lpCriticalSection

lpCriticalSection参数指向数据段中定义的CRITICAL_SECTION结构。

由于Windows操作系统保证临界区对象同时只能被一个线程进入,所以在需要独占操作的代码前加上进入临界区的操作,代码后面加上离开临界区的操作,就可以保证操作的独占性。在上面的例子中使用临界区对象进行同步的话,可以对计数线程进行如下修改:

invoke EnterCriticalSection,addr stCS  ;  stCS是CRITICAL_SECTION结构
inc dwCounter1
mov eax,dwCounter2
inc eax
mov dwCounter2,eax
invoke LeaveCriticalSection,addr stCS

进入临界区的操作由EnterCriticalSection函数来完成。如果当前有其他线程拥有临界区,函数不会返回,反之如果函数返回就表示现在可以独占数据了。调用EnterCriticalSection函数可以看成是让Windows检测标志,如果是“不允许”则等待;是“允许”则将标志修改为“不允许”状态并返回。

当完成操作的时候,还要将临界区交还Windows,以便其他线程可以申请使用,这个工作由LeaveCriticalSection函数完成,LeaveCriticalSection函数的功能可以看成是将标志从“不允许”改回“允许”状态。

当程序不再使用临界区的时候,必须使用DeleteCriticalSection将它删除:

invoke DeleteCriticalSection, lpCriticalSection

详细的代码也可以在所附光盘的Chapter12\ThreadSyn\UseCriticalSection目录中找到。读者可以自行查看其中的细节。

与事件对象不同,由于临界区对象无法命名,所以无法跨进程使用,但正是因为事件对象可以跨进程使用,需要占用的资源更多,所以相比之下临界区对象在速度上的优势很明显,上面的例子中,使用临界区对象进行同步的程序在10秒中之内可以计数81659524次,比使用事件对象快30多倍。

使用临界区的缺点在于,如果某个线程进入临界区后挂掉了,那么将无法被其他等待的线程检测到,因为这些线程都“陷”在EnterCriticalSection函数中了。但是使用事件对象时,可以在WaitForSingleObject函数中指定一个超时时间,当这个时间设置为一个很宽余的时间后,函数仍然因为超时而返回,那么就意味着某个线程挂掉了,程序就可以进行相关的处理。

在具体的使用中,使用哪种对象进行线程同步要看具体情况,一般在对速度要求比较高,并且不必跨进程进行同步的情况下,建议使用临界区对象。

【完整代码笔记】

; ThreadSyn.asm   ---------   使用临界区对象进行线程之间的同步
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ThreadSyn.asm
; rc ThreadSyn.rc
; Link  /subsystem:windows ThreadSyn.obj ThreadSyn.res
.386
.model flat,stdcall 
option casemap:none 

; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000 
IDC_COUNTER1 equ 1001 
IDC_COUNTER2 equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?

dwCounter1 	dword ?
dwCounter2 	dword ?

dwThreads 	dword ?
stCS 		CRITICAL_SECTION <?>
dwOption 	dword ?
F_STOP 		equ 0001h 

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	inc dwThreads 
	invoke SetWindowText, hWinCount, addr szStop 
	and dwOption, not F_STOP 
	
	.while !(dwOption & F_STOP)
		invoke EnterCriticalSection, addr stCS 
		inc dwCounter1 
		mov eax, dwCounter2 
		inc eax 
		mov dwCounter2, eax 
		invoke LeaveCriticalSection, addr stCS 
	.endw 
	dec dwThreads 
	invoke SetWindowText, hWinCount, addr szStart 
	ret 
_Counter endp 
 
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_TIMER 
		invoke EnterCriticalSection, addr stCS 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER1, dwCounter1, FALSE
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER2, dwCounter2, FALSE 
		invoke LeaveCriticalSection, addr stCS 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwThreads 
				or dwOption, F_STOP 
				invoke KillTimer, hWnd, 1 
			.else 
				mov dwCounter1, 0 
				mov dwCounter2, 0 
				xor ebx, ebx 
				.while ebx < 20 
					invoke CreateThread, NULL, 0, offset _Counter, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
					inc ebx 
				.endw 
				invoke SetTimer, hWnd, 1, 500, NULL 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		.if !dwThreads 
			invoke DeleteCriticalSection, addr stCS 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke InitializeCriticalSection, addr stCS 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

运行结果:

3.使用互斥量对象进行线程间同步

互斥量(Mutex)与临界区的作用相似,也是一种同时只允许一个线程获取的对象,但它是可以命名的,所以可以跨越进程使用。下面首先介绍对互斥量进行操作的函数。

互斥量可以用CreateMutex函数创建:

invoke CreateMutex,lpMutexAttributes,bInitialOwner,lpName
.if eax
    mov hMutex,eax
.endif

函数各参数的定义如下:

● lpMutexAttributes参数指向SECURITY_ATTRIBUTES结构,用来定义互斥对象的安全属性,如果事件对象的句柄不需要被继承,可以在这里指定NULL。

● bInitialOwner参数如果设置为TRUE,表示创建它的线程直接获取了该互斥量,设置为FALSE表示互斥量创建后处于空闲状态。

● lpName指向一个以0结尾的字符串,用来指定互斥对象的名称,如果为互斥对象命名,则对象可以在其他进程中用OpenMutix函数打开,以便用于进程间的线程同步。如果不需要命名,那么可以在这里使用NULL。

释放互斥量可以使用ReleaseMutex函数,但是调用该函数的线程必须首先拥有该互斥量,这一点和使用Event不同,在任何线程中都可以使用SetEvent函数来将Event对象置位:

invoke ReleaseMutex,hMutex

不需要再使用的时候,可以使用CloseHandle函数来关闭互斥量。

看到这里,读者可能会问,获取互斥量的函数呢?事实上系统中没有专门的类似于EnterMutex的函数,互斥量是靠WaitForSingleObject函数来获取的。当互斥量被某个线程获取(即等待成功)后,它的状态是复位的,否则是置位的。

在前面的例子中使用互斥量对象进行同步的话,首先用invoke CreateMutex,NULL,FALSE,NULL创建一个互斥量,然后对计数线程进行如下修改:

invoke WaitForSingleObject,hMutex,INFINITE
inc dwCounter1
mov eax,dwCounter2
inc eax
mov dwCounter2,eax
invoke ReleaseMutex,hMutex

WaitForSingleObject函数等待成功后会将互斥量置为复位,这样其他的线程就无法获取而继续等待,直到线程调用ReleaseMutex函数释放互斥量后,对象的状态变为置位,才会有另一个线程继续得到控制权。

详细的代码可以在所附光盘的Chapter12\ThreadSyn\UseMutex目录中找到。读者可以自行查看其中的细节。

使用互斥量进行同步的缺点和使用事件对象类似,也是在效率上远远低于使用临界区;当然优点也是类似的,那就是可以跨进程使用,可以检测到其他线程是否挂掉。

【完整代码笔记】

; ThreadSyn.asm   -------  使用互斥量对象进行线程之间的同步
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ThreadSyn.asm
; rc ThreadSyn.rc
; Link  /subsystem:windows ThreadSyn.obj ThreadSyn.res
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
 
; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000
IDC_COUNTER1 equ 1001 
IDC_COUNTER2 equ 1002 
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?

dwCounter1 	dword ?
dwCounter2  dword ?

dwThreads 	dword ?
hMutex 		dword ?
dwOption 	dword ?
F_STOP 		equ 0001h 

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	inc dwThreads 
	invoke SetWindowText, hWinCount, addr szStop 
	and dwOption, not F_STOP 
	
	.while !(dwOption & F_STOP)
		invoke WaitForSingleObject, hMutex, INFINITE 
		inc dwCounter1 
		mov eax, dwCounter2 
		inc eax 
		mov dwCounter2, eax 
		invoke ReleaseMutex, hMutex 
	.endw 
	dec dwThreads 
	invoke SetWindowText, hWinCount, addr szStart 
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_TIMER 
		invoke WaitForSingleObject, hMutex, INFINITE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER1, dwCounter1, FALSE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER2, dwCounter2, FALSE 
		invoke ReleaseMutex, hMutex 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwThreads 
				or dwOption, F_STOP 
				invoke KillTimer, hWnd, 1 
			.else 
				mov dwCounter1, 0
				mov dwCounter2, 0
				xor ebx, ebx 
				.while ebx < 20 
					invoke CreateThread, NULL, 0, offset _Counter, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
					inc ebx 
				.endw 
				invoke SetTimer, hWnd, 1, 500, NULL 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		.if !dwThreads 
			invoke CloseHandle, hMutex 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke CreateMutex, NULL, FALSE, NULL 
		mov hMutex, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main  

运行结果:

4.使用信号灯对象进行线程间同步

信号灯(Semaphore)对象是一个允许指定数量的线程获取的对象。信号灯对象一般用于线程排队,考虑这样一种情况:服务器程序设置了3个工作线程以及n个和客户端连接的服务线程,服务线程需要在工作线程空闲时与之通讯,但3个工作线程都忙的时候,就必须等待。这种情况下设置一个计数值为3的信号灯对象,服务线程和工作线程通讯前首先尝试获取信号灯,就可以保证同时只有3个服务线程可以通过,而其他线程处于等待状态。

当信号灯对象的计数值设置为1的时候,就相当于一个互斥量。

首先来看看和信号灯对象相关的函数,信号灯对象可以用CreateSemaphore函数创建:

invoke CreateSemaphore,lpSemaphoreAttributes,dwInitialCount,\
          dwMaximumCount,lpName
.if eax
    mov hSemaphore,eax
.endif

函数各参数的定义如下:

● lpSemaphoreAttributes参数指向SECURITY_ATTRIBUTES结构,用来定义信号灯对象的安全属性,如果事件对象的句柄不需要被继承,可以在这里指定NULL。

● dwInitialCount参数表示初始化的时候对象的计数值是多少,也就是说还有多少个线程允许获取对象,这个值必须大于等于0并且小于等于下面的dwMaximumCount参数。

● dwMaximumCount参数表示对象的最大计数值是多少,也就是最多允许多少个线程获取该对象。

● lpName指向一个以0结尾的字符串,用来指定对象的名称,如果为信号灯对象命名,则对象可以在其他进程中用OpenSemaphore函数打开,以便用于进程间的线程同步。如果不需要命名,那么可以在这里使用NULL。

释放信号灯对象使用ReleaseSemaphore函数,其中的dwReleaseCount指定释放的时候将计数值增加多少(一般使用1),lpPreviousCount指向一个双字变量,函数在此返回释放前的计数值,如果不需要返回这个值,可以将此参数设置为NULL:

invoke ReleaseSemaphore,hSemaphore,dwReleaseCount,lpPreviousCount

不需要再使用的时候,可以使用CloseHandle函数来关闭信号灯对象。

同样,信号灯对象是靠WaitForSingleObject函数来获取的。当对象被某个线程获取后,它的计数值减1,当计数值未减到0的时候,对象的状态是置位的,这意味着还有线程可以继续获取该对象;当计数值减到0的时候,对象的状态被复位。

在前面的例子中,如果使用信号灯对象进行同步,可以首先用invoke CreateSemaphore,NULL,1,1,NULL创建对象,对象的最大计数值和初始计数值都设置为1,然后对计数线程进行如下修改:

invoke   WaitForSingleObject,hSemaphore,INFINITE
inc      dwCounter1
mov      eax,dwCounter2
inc      eax
mov      dwCounter2,eax
invoke   ReleaseSemaphore,hSemaphore,1,NULL

WaitForSingleObject函数等待成功后会将信号灯对象的计数值减1,由于初始计数值是1,所以一旦有一个线程获取对象后,对象的状态即变为复位,其他的线程就无法获取而继续等待,直到线程调用ReleaseSemaphore函数将计数值增1为止,对象的状态变为置位,才会有另一个线程继续得到控制权。

详细的代码可以在所附光盘的Chapter12\ThreadSyn\UseSemaphore目录中找到。读者可以自行查看其中的细节。

【完整代码笔记】

; ThreadSyn.asm   -----------  使用互斥量对象进行线程之间的同步
;---------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ThreadSyn.asm
; rc ThreadSyn.rc
; Link  /subsystem:windows ThreadSyn.obj ThreadSyn.res
.386
.model flat,stdcall 
option casemap:none 

; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
 
; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000
IDC_COUNTER1 equ 1001
IDC_COUNTER2 equ 1002
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
hWinCount 	dword ?

dwCounter1 	dword ?
dwCounter2 	dword ?

dwThreads 	dword ?
hSemaphore 	dword ?
dwOption 	dword ?
F_STOP 		equ 0001h 

.const 
szStop 		byte '停止计数',0
szStart 	byte '计数',0
 
; 代码段
.code 
_Counter proc uses ebx esi edi, _lParam 
	inc dwThreads 
	invoke SetWindowText, hWinCount, addr szStop 
	and dwOption, not F_STOP 
	
	.while !(dwOption & F_STOP)
		invoke WaitForSingleObject, hSemaphore, INFINITE 
		inc dwCounter1 
		mov eax, dwCounter2 
		inc eax 
		mov dwCounter2, eax 
		invoke ReleaseSemaphore, hSemaphore, 1, NULL 
	.endw 
	dec dwThreads 
	invoke SetWindowText, hWinCount, addr szStart 
	ret 
_Counter endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @dwThreadID 
	
	mov eax, wMsg 
	.if eax == WM_TIMER 
		invoke WaitForSingleObject, hSemaphore, INFINITE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER1, dwCounter1, FALSE 
		invoke SetDlgItemInt, hWinMain, IDC_COUNTER2, dwCounter2, FALSE 
		invoke ReleaseSemaphore, hSemaphore, 1, NULL 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			.if dwThreads 
				or dwOption, F_STOP 
				invoke KillTimer, hWnd, 1 
			.else 
				mov dwCounter1, 0
				mov dwCounter2, 0
				xor eax,eax 
				.while ebx < 20
					invoke CreateThread, NULL, 0, offset _Counter, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
					inc ebx 
				.endw 
				invoke SetTimer, hWnd, 1, 500, NULL 
			.endif 
		.endif 
	.elseif eax == WM_CLOSE 
		.if !dwThreads 
			invoke CloseHandle, hSemaphore 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke GetDlgItem, hWnd, IDOK 
		mov hWinCount, eax 
		invoke CreateSemaphore, NULL, 1, 1, NULL 
		mov hSemaphore, eax 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

运行结果:


网站公告

今日签到

点亮在社区的每一天
去签到