📄 012.txt
字号:
这个程序很简单:当用户按下“计数”按钮的时候,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则改为如下代码:
.386
.model flat, stdcall
option casemap :none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Equ 等值定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_COUNTER equ 1001
IDC_PAUSE equ 1002
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWinMain dd ?
hWinCount dd ?
hWinPause dd ?
dwOption dd ?
F_PAUSE equ 0001h
F_STOP equ 0002h
F_COUNTING equ 0004h
.const
szStop db ~停止计数~,0
szStart db ~计数~,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
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,eax,DLG_MAIN,\
NULL,offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
将修改后的源程序和修改前的对比一下,可以发现不同的只有两个地方:第一是处理“计数”按钮的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,\
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -