用事件对象控制线程

之前的一篇博客里面用汇编写了一个多线程计数器,但是这个计数器存在一个缺陷,当我们按下暂停的时候,虽然看起来计数暂停,但是程序在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函数,在窗口过程的初始化里面创建一个事件对象。然后为了防止显示计数的那部分代码也出现不同步的现象,所以也做了和计数相同的处理。最后程序编译链接再运行:

可以看到任何时刻,计数都是一样的。

其实让线程同步的机制还有很多。比如临界区,互斥对象,信号灯。


 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值