12.2.2节中经过改进的多线程版的Counter程序运行起来一切正常,但是不知道读者有没有发现一个小缺点——在CPU时间占用上的小缺点。
如果程序在Windows NT系列操作系统中运行,就可以从任务管理器中发现这个问题(可以通过按下Ctrl+Alt+Del键调出任务管理器程序),如图12.2所示,当计数正在进行的时候,任务管理器显示Counter.exe程序的CPU占用率为96%,这没有什么奇怪,因为当前只有这一个程序在瞎忙活,并没有其他大运算量的程序,所以Counter.exe程序占用了绝大部分的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函数删除事件对象。
修改后的代码在所附光盘的Chapter12Event目录中,Counter.rc文件并没有改动。改动后的Counter.asm文件如下:
.386
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 ?
hEvent 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 SetEvent,hEvent
invoke SetWindowText,hWinCount,addr szStop
invoke EnableWindow,hWinPause,TRUE
xor ebx,ebx
! (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
;********************************************************************
eax == WM_COMMAND
mov eax,wParam
ax == IDOK
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
ax == IDC_PAUSE
xor dwOption,F_PAUSE
dwOption & F_PAUSE
invoke ResetEvent,hEvent
.else
invoke SetEvent,hEvent
.endif
.endif
;********************************************************************
eax == WM_CLOSE
invoke CloseHandle,hEvent
invoke EndDialog,hWnd,NULL
;********************************************************************
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
invoke GetModuleHandle,NULL mov hInstance,eax invoke DialogBoxParam,eax,DLG_MAIN, NULL,offset _ProcDlgMain,NULL invoke ExitProcess,NULL;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> end start :
原文始发于微信公众号(汇编语言):12.3 使用事件对象控制线程
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论