12.3 使用事件对象控制线程

admin 2022年5月8日13:35:39评论28 views字数 6974阅读23分14秒阅读模式

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

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


12.3 使用事件对象控制线程

现在按下“暂停/恢复”按钮将计数暂停,就可以看出问题来了——即使计数暂停了,但是程序的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                    .model flat, stdcall                    option casemap :none;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Include 文件定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>include         windows.incinclude         user32.incincludelib      user32.libinclude         kernel32.incincludelib      kernel32.lib;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Equ 等值定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN            equ     1000DLG_MAIN            equ     1000IDC_COUNTER     equ     1001IDC_PAUSE       equ     1002;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 数据段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                    .data?hInstance       dd      ?hWinMain            dd      ?hWinCount       dd      ?hWinPause       dd      ?hEvent          dd      ?       ;事件对象句柄 dwOption            dd      ?F_PAUSE         equ     0001hF_STOP          equ     0002hF_COUNTING      equ     0004h                    .constszStop          db      '停止计数',0szStart         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                    .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;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>start:                invoke  GetModuleHandle,NULL                mov     hInstance,eax                invoke  DialogBoxParam,eax,DLG_MAIN,                        NULL,offset _ProcDlgMain,NULL                invoke  ExitProcess,NULL;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                end     start


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


12.3 使用事件对象控制线程



原文始发于微信公众号(汇编语言):12.3 使用事件对象控制线程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月8日13:35:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   12.3 使用事件对象控制线程https://cn-sec.com/archives/985957.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息