WIN32汇编语言教程:第12章 多线程 · 12.3 使用事件对象控制线程(1)
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章 多线程 · 12.2 多线程编程(4) 下页:第12章 多线程 · 12.3 使用事件对象控制线程(2)