WIN32汇编语言教程:第12章 多线程 · 12.2 多线程编程(2)
这个程序很简单:当用户按下“计数”按钮的时候,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秒,那么就最好采取别的方法来完成,第04章中介绍的在消息循环中使用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