⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 012.txt

📁 会变语言实现的一些程序
💻 TXT
📖 第 1 页 / 共 5 页
字号:
  dwParameter,dwCreationFlags,lpThreadId

  .if eax

  mov hThread,eax

  .endif

函数使用的参数定义如下:

● lpThreadAttributes——指向一个SECURITY_ATTRIBUTES结构,用来定义线程的安全属性,这个结构在CreateFile函数的介绍中已经涉及过,主要用来指定句柄是否可以继承,如果想让线程使用默认的安全属性,可以将参数设置为NULL。

● dwStackSize——线程的堆栈大小。如果指定为0,那么线程的堆栈大小和主线程使用的大小相同。系统自动在进程的地址空间中为每个新线程分配私有的堆栈空间,这些空间在线程结束的时候自动被系统释放,如果需要的话,堆栈空间会自动增长。

● lpStartAddress——线程开始执行的地址。这个地址是一个规定格式的函数的入口地址,这个函数就被称为“线程函数”。

● dwParameter——传递给线程函数的自定义参数。

● dwCreationFlags——创建标志。如果是0,表示线程被创建后立即开始运行,如果指定CREATE_SUSPENDED标志,表示线程被创建后处于挂起状态,直到使用ResumeThread函数显式地启动线程为止。

● lpThreadId——指向一个双字变量,用来接收函数返回的线程ID。线程ID在系统范围内是惟一的,一些函数需要用到线程ID。

如果线程创建成功,函数返回一个线程句柄,这个句柄可以用在一些控制线程的函数中,如SuspendThread,ResumeThread和TerminateThread等函数,如果线程创建失败,那么函数返回NULL。

当程序调用CreateThread函数时,首先系统为线程建立一个用来管理线程的数据结构,其中包含线程的一些统计信息,如引用计数和退出码等,这个数据结构被称为线程对象;接下来系统将从进程的地址空间中为线程的堆栈分配内存并开始线程的执行。当线程结束时,线程的堆栈被释放,但是线程对象不会马上被释放,系统保留它以便其他线程可以通过它检测线程的有关情况,直到使用CloseHandle函数关闭线程句柄后,线程对象才会被释放。

但是线程对象也可以提前被释放,对于大部分的句柄来说(如文件句柄hFile,文件寻找句柄hFindFile等),使用CloseHandle函数关闭句柄意味着整个对象被释放,但对于线程句柄来说,关闭它仅释放线程的统计信息,并不会终止线程的执行,所以如果不再需要使用线程句柄的话,在调用CreateThread后马上就可以将它关闭掉,线程的执行并不会受影响。

 



2. 线程函数

如果创建线程时没有指定CREATE_SUSPENDED标志,当CreateThread函数返回时,lpStartAddress参数指向的线程函数就已经开始运行了。线程函数包含所有需要在线程中执行的代码,它有一个输入参数,线程函数的一般书写格式是:

_ProcThread proc   uses ebx esi edi lParam

  local 局部变量

  

  ...

  mov eax,返回码

  ret

 

_ProcThread endp

读者可以自由定义函数的名称,只要在使用CreateThread函数时将lpStartAddress参数指向函数的入口地址就可以了,lParam参数传递过来的就是调用CreateThread函数时使用的dwParameter参数。

向线程函数传递参数的时候,读者可能会觉得一个lParam参数不太够用,如果需要传递多个参数该怎么办呢?其实这不是问题,因为子线程和主线程使用同一个地址空间,主线程可以通过全局变量来传递参数。

有时候也可能遇到这种情况:进程中存在多个子线程,这些子线程的线程函数使用同一个子程序,如果对这些子线程使用同样的全局变量传递参数,难免会引起冲突。这时可以为每个子线程分配一个存放参数的内存块,主线程通过lParam参数把内存块的指针传递给子线程,子线程通过这个指针存取内存块中的内容就可以了,不过在子线程结束的时候不要忘了释放内存块。

3. 终止线程

线程从线程函数的第一句代码开始执行,直到线程被终止为止。当线程被正常终止时,系统会进行下面的操作:

● 线程使用的堆栈被释放。

● 系统将线程对象中的退出代码设置为线程的退出码。

● 系统将递减线程对象的使用计数。

线程结束后的退出码可以被其他线程用GetExitCodeThread函数检测到,所以可以当做自定义的返回值来表示线程执行的结果。终止一个线程的执行有4种方法。

第1种方法是线程函数的自然退出,当函数执行到一句ret指令返回时,Windows将终止线程的执行,这时放在eax中的返回值就是线程的退出码。一般建议使用这种方法终止一个线程的执行。

第2种方法是使用ExitThread函数来终止线程:

  invoke  ExitThread,dwExitCode

ExitThread函数只能用于终止当前线程,它并不能用于在一个线程中终止另外一个线程,和ExitProcess函数一样,ExitThread函数不会有返回的时候。dwExitCode参数指定为线程的退出码。使用ExitThread函数和使用ret指令终止线程的效果是一样的,但显然不如使用ret指令来得简洁和方便。

第3种方法是使用TerminateThread函数,这个函数可以用来在一个线程中强制终止另一个线程的执行:

  invoke  TerminateThread,hThread,dwExitCode

hThread参数指定需要终止的线程句柄,dwExitCode将用做被终止线程的退出码。如果函数执行成功,返回值是非0值,否则函数返回0,但是TerminateThread函数是一个异步执行的函数,即使函数返回非0值,也并不代表目标线程已经终止,可能终止的过程还要延续一段时间,如果必须确认线程已经真正结束的话,可以使用GetExitCodeThread函数来检测。

TerminateThread函数是一个被强烈建议避免使用的函数,因为一旦执行这个函数,程序无法预测目标线程会在何处被终止,其结果就是目标线程可能根本没有机会来做清除工作。读者可以尝试在Counter.asm例子中使用TerminateThread函数来终止_Counter线程的执行。可以发现计数线程是停止了,但是“停止计数”按钮并不会恢复为“计数”按钮,“暂停/恢复”按钮也不会被灰化。因为计数线程平时在循环中执行,被强制终止的时候必然还在循环体内,这样下面的扫尾代码将没有机会执行,其结果当然如此了:

  invoke  SetWindowText,hWinCount,addr szStart

  invoke  EnableWindow,hWinPause,FALSE

  and   dwOption,not (F_COUNTING or F_STOP or F_PAUSE)

TerminateThread函数引发的问题可能还有很多,如线程中打开的文件和申请的内存等都不会被释放,更危险的是,如果线程刚好在调用Kernel32.dll中的系统函数时被终止,可能会引起Kernel32的状态处于不正确的状态(当然只是线程所属进程的Kernel32状态而不是系统范围的状态)。另外,当使用TerminateThread函数终止线程的时候,系统不会释放线程使用的堆栈。所以建议读者在编程中的时候尽量让线程自己退出,如果主线程要求某个线程结束,可以通过各种方法通知线程,线程收到通知在做扫尾工作后自行退出。只有在迫不得已的情况下,才能使用TerminateThread函数去终止一个线程。

第4种方法就是使用ExitProcess函数结束进程,这时系统会自动结束进程中所有线程的运行。在以前演示的所有的单线程程序中,并不显式地结束主线程的运行,而总是用直接结束进程的方法让主线程自然结束。在多线程的程序中,用这种方法结束线程相当于对每个线程使用TerminateThread函数,所以也应当避免这种情况(用这种方法结束主线程的运行并不是问题,因为在这之前可以预测到线程的结束并进行扫尾工作)。

当一个线程终止时,Windows释放执行线程所需的各种资源,如堆栈与寄存器环境等,并且不再继续分配时间片调用线程中的代码,但线程对象并不马上被释放,因为以后其他线程可能还需要用GetExitCodeThread函数检测线程的退出码。线程对象一直保存到使用CloseHandle函数关闭线程句柄为止。

4. 其他相关函数

除了上面介绍的一些函数,读者还可以通过其他的相关函数对线程进行控制。下面简单介绍SuspendThread,ResumeThread和GetExitCodeThread函数的用法。

一个线程可以被挂起(暂停),也可以在挂起后被恢复执行。当使用CreateThread函数创建线程的时候,如果在dwCreationFlags参数中指定CREATE_SUSPENDED标志,线程创建后并不马上开始执行,而是处于被挂起的状态,直到使用ResumeThread函数启动它为止。除了在创建的时候直接让线程处于挂起状态,也可以使用SuspendThread函数将运行中的线程挂起:

  invoke  SuspendThread,hThread

该函数的惟一参数是需要挂起的线程句柄。系统为每个线程维护一个暂停计数器,SuspendThread函数将导致线程的暂停计数增加,当一个线程的暂停计数大于0的时候,系统就不会给线程安排时间片,这就相当于将线程挂起,如果函数执行成功,返回值是线程原来的暂停计数值,当函数执行失败时,返回值是–1。如果创建线程的时候使用CREATE_SUSPENDED标志,那么线程的暂停计数值一开始就是1。

要将挂起的线程恢复到执行状态,可以使用ResumeThread函数:

  invoke  ResumeThread,hThread

该函数减少线程的暂停计数,当计数值减到0的时候,线程被恢复运行,所以函数被调用后线程是否被恢复运行还要看原来的暂停计数值是多少,如果多次调用SuspendThread函数导致暂停计数值远远大于1的话,就必须多次调用ResumeThread后线程才能被恢复运行。ResumeThread的返回值定义和SuspendThread函数的定义是一样的。

一个线程可以将别的线程挂起,也可以将自己挂起,但是将自己挂起后,显然不可能再由自己来恢复运行,因为这时线程不可能再运行ResumeThread函数了,在这种情况下,必须由其他线程来进行恢复操作。

在例子程序中,也可以将“暂停/恢复”的功能通过挂起/恢复来实现。

GetExitCodeThread函数用来获取线程的退出码,同时也可以用来检测线程是否已经结束。函数的用法是:

  invoke  GetExitCodeThread,hThread,lpExitCode

其中hThread参数指定需要获取的线程句柄,lpExitCode指向一个双字变量,用来接收函数返回的退出信息,如果函数执行成功,返回非0值,并且将退出码返回到lpExitCode指向的变量中,如果执行失败,函数返回0。

当一个线程没有结束的时候,退出信息中返回的是STILL_ACTIVE,如果线程已经结束,那么变量中返回的就是线程的退出码,通过检查退出信息是否为STILL_ACTIVE就可以得知线程是否已经结束。

 






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.3 使用事件对象控制线程(2)


12.3.3  进一步改进计数程序 
现在让我们进一步改进前面的计数程序,用事件对象代替暂停标志,用WaitForSingleObject函数代替测试暂停标志的语句,这样就可以解决CPU占用率的问题。改进的步骤如下:

● 在程序初始化的时候用CreateEvent函数建立事件对象,以便当做暂停标志使用。

● 当计数线程开始的时候,使用SetEvent函数将事件的初始状态设置为“置位”。

● 计数循环中使用WaitForSingleObject函数测试事件状态,当不需要暂停的时候,由于事件的状态为“置位”,函数会马上返回,循环继续执行。主线程中通过使用ResetEvent函数将事件复位来暂停线程,因为这时进入WaitForSingleObject函数后不会返回,直到主线程中继续使用SetEvent函数将事件置位为止。

● 退出程序的时候用CloseHandle函数删除事件对象。

修改后的代码在所附光盘的Chapter12\Event目录中,Counter.rc文件并没有改动。改动后的Counter.asm文件如下:

        .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   1000

DLG_MAIN     equ   1000

IDC_COUNTER   equ   1001

IDC_PAUSE   equ   1002

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

; 数据段

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance   dd    ?

hWinMain     dd    ?

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -