时间片的概念大家应该都了解过,windows为每个进程分配时间片,当一个程序的时间片结束时,就会轮到下一个程序执行,当轮流速度比较快的时候,就好像多个进程同时执行一样,这就是windows多任务的方式,本次介绍的多线程其实跟这个很像,只不过线程是在进程内又划分的更小的可执行单位,windows为每个线程分配时间片,当轮到某个进程的某个线程执行的时候,该线程才开始执行,就好像是进程间的多任务一样,线程依赖于进程,进程结束则线程必定结束,线程结束则进程不一定结束,线程就是一个进程中的某个可执行的模块,有单独完成某项功能的作用,正因为如此才会出现多线程编程,我现在理解的多线程编程就是,可以将某个需要长时间处理的子模块单独在一个线程中完成,比如说一个线程处理带有界面的子模块,如果在这个线程中加入一些需要长时间运算的功能代码的话,那么这个界面就会卡到那(就如同我们平时遇见的某个程序的界面未响应,卡到那没有反映一样,这个很烦人吧),如果将需要计算的那个功能放在一个独立的模块,建立一个新的线程执行,将结果传递到那个需要处理界面的线程的模块中,这样就不会出现那种卡顿的现象了(对于现在的计算机一班都是多处理器,这样会有更快的处理速度),下面来介绍一下利用多线程与事件实现一个计数器。首先看一下程序的流程:
图标 | IDI_ICON2 |
对话框 | IDD_DIALOG1 |
静态文本 | 不需要ID,不需要对其操作 |
编辑文本 | IDC_COUNTER |
按钮 | IDOK IDC_PAUSE |
计数函数 | _COUNTER |
对画框消息处理 | _ProcDlgMain |
模态对话框 | DialogBoxParam |
首先是资源文件的编写,依然使用ResEdit工具编写,资源文件编写不在赘述,很简单注意风格就行,直接看一下代码:
// Generated by ResEdit 1.6.6
// Copyright (C) 2006-2015
// http://www.resedit.net
#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
#include "resource.h"
//
// Dialog resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_DIALOG1 DIALOG 0, 0, 186, 95
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "多线程与事件"
FONT 8, "Ms Shell Dlg"
{
EDITTEXT IDC_COUNTER, 55, 20, 124, 16, ES_AUTOHSCROLL, WS_EX_LEFT
LTEXT "计数值显示:", 0, 2, 24, 49, 9, SS_LEFT, WS_EX_LEFT
PUSHBUTTON "暂停/恢复", IDC_PAUSE, 117, 65, 48, 14, 0, WS_EX_LEFT
DEFPUSHBUTTON "计数", IDOK, 21, 66, 48, 14, 0, WS_EX_LEFT
}
//
// Icon resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDI_ICON2 ICON "icon2.ico"
下面来看一下程序实现部分,这次程序编写遇到好多问题,有历史问题还有新的问题,首先看一下历史问题(以前理解的总感觉不是很扎实,不稳固,有点迷茫的感觉)今天彻底梳理一下,这个问题就是关于设置标志的问题,关于设置标志,有什么用呢,其实就是为了方便判断以及操作而设置的一个标志而已,有时标志会根据对话框按钮的状态来设置,比如本次程序的下面的三个标志,只说不练也是徒劳,下面来根据具体的使用来学习一下.
本次程序中使用的标志变量是dwOption 设置的三个标志分别为:
F_PAUSE equ 0001h ;二进制为:0001 (只写16进制的最后一位 为2进制)
F_STOP equ 0002h ;二进制为:0010
F_COUNTING equ 0004h ;二进制为:0100
首先来看一下第一个例子也就是计数模块:
Counter proc
or dwOption,F_COUNTING ;初始状态dwOption为0,dwOption(0) 与F_COUNTING(0100)
按位相或结果为:0100 (其实也就是置位)
and dwOption,not (F_STOP or F_PAUSE) ;上句代码之后 dwOption(0100)与 not(F_STOP or F_PAUSE )按位相与。首先来看
F_STOP(0010) 与 F_PAUSE(0001) 按位相或 结果
为:0011 再加上not(0011)---》(1100) 然 后dwOption(0100)与(1100) 按位相与,结果
为0100 也就是F_COUNTING
总结:上面这两句代码的意思其实就是为了将
dwOption设置为F_COUNTING,从第二句可以知 道not (F_STOP or F_PAUSE) 结果为(1100)
F_STOP 与F_PAUSE F_COUNTING与
(1100)相与之后,只有F_COUNTING 有效, 其他两个直接无效,而第一句正好将dwOption设置
为F_COUNTING,也就是代表现在的IDOK按钮的 显示的状态为 计数中
invoke SetEvent,hEvent
invoke SetWindowText,hWinCount,addr szStop
invoke EnableWindow,hWinPause,TRUE
xor ebx,ebx
.while ! (dwOption & F_STOP) ;dwOption与F_STOP相同则为真 ,!真即是假(false) 则跳出循环,如果为其它标志,则为假,!假即是真,
执行循环语句,注意这个不是按位操作(个人理解)
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_PAUSE or F_STOP) ;最后利用按位操作符将dwOption
复位(就是不再是任何一种标志)
ret
_Counter endp
下面来看一下第二个问题,这个问题是个新的问题,那就是事件对象的用法,事件对象可以看做一个设置在Windows内部的标志,它的状态设置和测试工作由windows来完成,windows可以将这些标志的设置和测试工作和线程调度等工作在内部结合起来,这样效率很高。事件可以分为两种状态:置位 和 复位 这两个操作可以通过SetEvent 和 ResetEvent 两个函数分别实现函数(从英文字面意思也可以看出来,谁让这是外国人写的呢。。),这其实和上一个问题中的使用
or 和 and 操作是标志变量 置位和复位的操作一样。如果将事件看成是“标志”的话,可以用WaitForSingleObject函数来检测,这个函数有一点 特殊,那就是当该函数返回时,需要满足两种状态:
1.测试事件对象的状态为置为状态
2.到了dwMilliseconds指定的超时时间 (这个是这个函数的一个参数,详见后面对函数的介绍)
这样,我们可以在程序初始化的时候建立事件对象,当作标志用,然后我们可以在对话框的消息处理中,当接收到IDOK按钮的消息时,将事件对象设置为 置位。然后在计数模块中使用WaitForSingleObject 函数测试事件的状态,当状态为置位时,函数可以正常返回,否则不会返回,函数会等待,不再出现循环测试标志位的情况,而主要依靠的是事件的状态情况。
下面来看一下具体的实现代码:
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
IDI_ICON2 equ 101
IDD_DIALOG1 equ 102
IDC_COUNTER equ 40001
IDC_PAUSE equ 40002
.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
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_PAUSE or F_STOP)
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,IDD_DIALOG1,NULL,offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
下面来介绍一下几个API函数:
CreateEvent()
功能:
它用来创建或打开一个命名的或无名的事件对象。
原型:
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,// 安全属性
BOOLbManualReset,// 复位方式
BOOLbInitialState,// 初始状态
LPCTSTRlpName // 对象名称
);
参数:
lpEventAttributes
一个指向SECURITY_ATTRIBUTES结构的指针,确定返回的句柄是否可被子进程继承。如果lpEventAttributes是NULL,此句柄不能被继承。
Windows NT/2000:lpEventAttributes的结构中的成员为新的事件指定了一个安全符。如果lpEventAttributes是NULL,事件将获得一个默认的安全符。
bManualReset
指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当一个等待线程被释放以后,系统将会自动将事件状态复原为无信号状态。
bInitialState
指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态。
lpName
指定事件的对象的名称,是一个以0结束的字符串指针。名称的字符格式限定在MAX_PATH之内。名字是对大小写敏感的。
如果lpName指定的名字,与一个存在的命名的事件对象的名称相同,函数将请求EVENT_ALL_ACCESS来访问存在的对象。这时候,由于bManualReset和bInitialState参数已经在创建事件的进程中设置,这两个参数将被忽略。如果lpEventAttributes是参数不是NULL,它将确定此句柄是否可以被继承,但是其安全描述符成员将被忽略。
如果lpName为NULL,将创建一个无名的事件对象。
如果lpName的和一个存在的信号、互斥、等待计时器、作业或者是文件映射对象名称相同,函数将会失败,在GetLastError函数中将返回ERROR_INVALID_HANDLE。造成这种现象的原因是这些对象共享同一个命名空间。
返回值:
如果函数调用成功,函数返回事件对象的句柄。如果对于命名的对象,在函数调用前已经被创建,函数将返回存在的事件对象的句柄,而且在GetLastError函数中返回ERROR_ALREADY_EXISTS。
WaitForSingleObject()
功能:
用来测试事件,线程,和进程等对象的状态
原型:
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);
参数:
hHandle[in]对象句柄。可以指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
当等待仍在挂起状态时,句柄被关闭,那么函数行为是未定义的。该句柄必须具有 SYNCHRONIZE 访问权限。
dwMilliseconds[in]定时时间间隔,单位为milliseconds(毫秒).如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。
返回值:
WAIT_ABANDONED 0x00000080:当hHandle为mutex时,如果拥有mutex的线程在结束时没有释放核心对象会引发此返回值。
WAIT_OBJECT_0 0x00000000 :核心对象已被激活
WAIT_TIMEOUT 0x00000102:等待超时
WAIT_FAILED 0xFFFFFFFF :出现错误
SetEvent()
功能:
设置事件的状态为有标记,释放任意等待线程
原型:
BOOL SetEvent(HANDLE hEvent);
参数;
hEvent表示事件句柄
返回值:
如果操作成功,则返回非零值,否则为0。
ResetEvent()
功能:
这个函数把指定的事件对象设置为无信号状态(就是复位)
原型:
BOOL ResetEvent(
HANDLE hEvent
);
参数:
hEvent
[in] 指向事件对象的句柄.由 CreateEvent or OpenEvent 函数返回。 这个句柄需要拥有EVENT_MODIFY_STATE 访问权限.
返回值:
函数成功,返回非0值,否则返回0值,可以调用GetLastError得到错误的详细信息
CreateDialogParam()
功能:
函数根据对话框模板资源创建一个无模式(非模态)的对话框。在显示对话框之前,函数把一个应用程序定义的值作为WM_INITDIALOG消息IParam参数传到对话框过程应用程序可用此值来初始化对话框控制
原型:
HWND CreateDialogParam(HINSTANCE
hlnstancem,LPCTSTR IpTemplateName,HWND hWndParent,DLGPROCIpDialogFunc, LPARAM dwlniParam);
参数:
hlnstance:标识一个模块的事例,该模块的可执行文件含有对话框模板。
IpTemplateName:标识对话框模板。此参数可以指向一个以NULL结尾的字符串的指针,该字符串指定对话框模板名,或是指定对话框模板的资源标识符的一个整型值。如果此参数指定了一个资源标识符,则它的高位字一定为零且低位字一定含有标识符。一定用MAKEINTRESOURCE宏指令创建此值。
HwndParent:指定拥有对话框的窗口。
IpDialogFunc:指向对话框过程的指针。有关对话框过程的更详细的信息,请参见DialogProc。
dwlnitParam:指定传递到WM_INITDIALOG消息的IParam参数中的对话框过程的值。
返回值:
如果函数调用成功则返回值为指向对话框的窗口句柄。如果函数调用失败则返回值为NULL。