之前的一篇博客里面用汇编写了一个多线程计数器,但是这个计数器存在一个缺陷,当我们按下暂停的时候,虽然看起来计数暂停,但是程序在CPU里面的占用几乎没有变,这是因为循环还在继续。所以如果有一种机制,能够让我们按下暂停的时候,线程里面的代码就停住了。这样就不会继续占用CPU资源。
这种机制是有的,那就是“事件”。事件也是一个对象,类似于文件,窗口,内存等对象。我们可以把“事件”看成一个标志,一个设置在Windows内部的标志。这个标志,也就是“事件”,有两种状态,一个是“置位”状态,一个是“复位”状态。
这个对象需要用一个函数去创建。
CreateEvent
invoke CreateEvent,lpEventAttributes,bManualReset,bInitialState,lpName
.if eax
mov hEvent,eax
.endif
该函数创建一个“事件”对象,函数执行成功返回一个事件对象句柄。参数定义如下:
- lpEventAttributes:该参数指向一个SECURITY_ATTRIBUTES结构,它用来定义事件对象的安全属性,如果该对象句柄不需要被继承,那么这个参数定义为NULL就可以了。
- bManualReset:该参数指定“事件”是否需要手动复位。之前有说到“事件”有置位和复位两种状态。如果这个参数指定为TRUE,那么“事件”的复位就必须使用函数手动完成。如果指定为FALSE,当测试事件的函数返回的时候,“事件”就会自动被复位。
- bInitialState:该参数指定事件对象创建的时候的初始状态,TRUE表示事件初始是置位的,FLASE表示事件初始状态是置位的。
- lpName:该参数指向一个以0结尾的字符串,它用来指定事件对象的名称,因为事件对象可以在其他地方通过事件对象名获得事件对象的句柄而被使用。
上面这个函数里面有个参数有提到手动置位的问题,那么肯定就有函数能够控制事件的状态了。
SetEvent
invoke SetEvent,hEvent
该函数设置事件为置位。参数是事件对象句柄。
ResetEvent
invoke ResetEvent,hEvent
该函数设置事件为复位。参数是事件对象句柄。
之前创建事件对象的函数的参数里面还提到一个测试事件的函数。该函数的函数名字叫WaitForSingleObject,字面上看起来并没有测试的意思,反而有等待的意思,的确用起来的时候感觉就是在等待。下面是该函数的定义:
WaitForSingleObject
invoke WaitForSingleObject,hHandle,dwMilliseconds
当代码执行到这个函数的时候,如果事件是置位状态,那么该函数正常执行然后返回,如果事件是复位状态,那么在这个线程里面,执行就会停在这个函数里面,函数不会返回,就像是代码的执行停在这里了,当然,并不是在函数里面做循环,要不然又和之前的程序一样了。代码在我们看来确实就是停止执行了,cpu里面也没有占用。只有当事件变为置为状态,函数才会返回然后继续执行后面的代码。
函数的第一个参数为要测试的事件对象的句柄,第二个参数指定以毫秒为单位的超时时间。该函数有两种情况下会返回,第一种就是事件变为置为状态,第二种就是时间到了这个参数规定的时间。如果这个参数是0,那么函数测试事件对象后马上返回。如果需要函数无限期等待,那么参数可以使用INFINITE预定义值。
该函数不仅能测试事件对象,它还能测试线程,进程还有其他的一些对象。下面给出该函数支持测试的部分对象的状态的定义:
- 控制台输入:如果用户的输入使得控制台的缓冲区不为空的时候,控制台对象的状态就变为“置位”。如果缓冲区为空,那么状态就为“复位”。
- 事件对象:这个前面讲了,对事件对象调用SetEvent函数后,状态为“置位”。当事件对象调用ResetEvent函数之后,状态为“复位”。
- 进程对象:如果进程结束,状态为“置位”。
- 线程对象:如果线程结束,状态为”置位“。
使用这个函数我们就不用用一些标志位来检测是否按下了暂停键,只要在计数的循环内放置一个这个函数,如果用户按下暂停键,直接将事件设置为复位,然后计数的线程执行就会停在这个函数里面,并且不会占用CPU。
上面这个函数只能测试一个对象,我们还有能够测试多个对象的函数:
WaitForMultipleObjects
invoke WaitForMultipleObjects,dwCount,lpHandles,bWaitAll,dwMilliseconds
dwCount参数指定对象句柄的数量。lpHandles参数指向一组对象句柄变量。函数会同时测试这些对象句柄的状态。bWaitAll参数只当测试的逻辑,如果指定为TRUE,那么函数只有在所有的对象都变为置位的时候才会返回,如果指定为FALSE,那么只要有一个事件对象变为置位函数就会返回。最后一个参数也是一个超时时间,和上一个函数的用法一样。
那么之前的程序用了“事件”之后,我们再来测试一下。程序的代码就不再贴出来了,因为只需要在计数循环里面添加一个测试事件对象的函数,然后在窗口过程那里在暂停消息下面使用ResetEvent函数,继续消息下面使用SetEvent函数就可以了。
程序编译链接后执行,然后再按下暂停键:
可以看到最下面那个是我们的计数程序,按下暂停后,CPU的占用也变成0了。
分割线----------------------------------------------------------------------------------------------------------------------------
之前既然有说到,用多线程一个好处就是可以加快处理,那么如果我们多创建几个线程来进行计数,那岂不是快得飞起。这么多线程只用来计数一个计数器好像有点浪费,那么我们计数两个看看。下面就是两个计数器的代码,该程序用了一个计时器,每500毫秒发送一次消息,然后窗口过程收到计数消息就依次把两个计数显示出来。然后计数是用了两个全局变量。也是用一个循环。
.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 1000h
DLG_MAIN equ 1000h
IDC_COUNTER1 equ 1001h
IDC_COUNTER2 equ 1002h
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWinMain dd ?
hWinCount dd ?
hEvent dd ?
dwThreads dd ?
dwOption dd ?
F_STOP equ 0001h
dwCounter1 dd ?
dwCounter2 dd ?
szCs CRITICAL_SECTION <>
.const
szStop db '停止计数',0
szStart db '计数',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Counter proc uses ebx esi edi _lParam
inc dwThreads
invoke SetWindowText,hWinCount,addr szStop
and dwOption,not F_STOP
.while ! (dwOption & F_STOP)
inc dwCounter1
mov eax,dwCounter2
inc eax
mov dwCounter2,eax
.endw
mov dwThreads,0
invoke SetWindowText,hWinCount,addr szStart
ret
_Counter endp
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
local @dwThreadID
mov eax,wMsg
.if eax == WM_TIMER
invoke SetDlgItemInt,hWinMain,IDC_COUNTER1,\
dwCounter1,FALSE
invoke SetDlgItemInt,hWinMain,IDC_COUNTER2,\
dwCounter2,FALSE
.elseif eax == WM_COMMAND
mov eax,wParam
.if eax == IDOK
.if dwThreads
or dwOption,F_STOP
invoke KillTimer,hWnd,1
.else
mov dwCounter1,0
mov dwCounter2,0
xor ebx,ebx
.while ebx < 10 ;创建十个线程
invoke CreateThread,NULL,0,\
offset _Counter,NULL,\
NULL,addr @dwThreadID
invoke CloseHandle,eax
inc ebx
.endw
invoke SetTimer,hWnd,1,500,NULL
.endif
.endif
.elseif eax == WM_CLOSE
.if !dwThreads
invoke EndDialog,hWnd,NULL
.endif
.elseif eax == WM_INITDIALOG
push hWnd
pop hWinMain
invoke GetDlgItem,hWnd,IDOK
mov hWinCount,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
资源文件:
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 0x1000
#define DLG_MAIN 0x1000
#define IDC_COUNTER1 0x1001
#define IDC_COUNTER2 0x1002
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 227,187,129,56
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "多线程同步演示程序"
FONT 9,"宋体"
BEGIN
LTEXT "计数器一: ",-1,7,7,40,8
LTEXT "计数器二: ",-1,7,22,41,8
EDITTEXT IDC_COUNTER1,51,5,71,12,ES_READONLY | WS_BORDER | WS_TABSTOP
EDITTEXT IDC_COUNTER2,51,20,71,12,ES_READONLY | WS_BORDER | WS_TABSTOP
PUSHBUTTON "计数",IDOK,72,36,50,14
END
编译链接后运行:
发现出了一个奇怪的问题,两个计数明明是在同一个线程里面,同一个循环里面计数的,为什么结果会显示不一样呢?
原因就是:虽然这两个计数是同一个线程同一个循环里面的,但是这两个计数也有先后之分,并且两个计数用的是全局变量,可能在第一个计数加一后,系统就切换到另一个线程里去执行了,然后在另一个线程里面又对第一个计数加一,而且我们有十个线程,时间一长。两个计数的差距就会很大。
这种问题就是线程的同步问题。因为线程在执行时随时可能会被切换,这样的话对于计数还有一些其他的需要同步的事情来说就是一个很大的隐患。那么我们就需要让某个我们需要同步的事情要独占整个过程,也就是必须让它做完整个事情,其他线程才能开始做这个相同的事情。
正好,之前的测试对象函数里面有个参数就可以指定当函数返回时,事件自动复位。这样,我们就可以在需要同步的事情的代码开始执行前用这个函数,然后在事情的代码的结尾再用SetEvent函数置位。这样一来,就算我们需要同步的事情在执行的的时候被切换到别的线程了,别的线程也执行不了这一部分的代码,只有当最开始执行那个事情的代码执行完之后用SetEvent函数将事件置位,别的线程才可以开始执行这一部分的代码。
那么,只要将上面的代码做一个简单的修改,将测试事件函数放在计数代码的最前面,并将参数bManualReset设置为FALSE,最后面放一个SetEvent函数,在窗口过程的初始化里面创建一个事件对象。然后为了防止显示计数的那部分代码也出现不同步的现象,所以也做了和计数相同的处理。最后程序编译链接再运行:
可以看到任何时刻,计数都是一样的。
其实让线程同步的机制还有很多。比如临界区,互斥对象,信号灯。