WIN32汇编语言教程:第12章 多线程 · 12.2 多线程编程(4)

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章 多线程 · 12.2 多线程编程(3) 下页:第12章 多线程 · 12.3 使用事件对象控制线程(1)

第12章 多线程

版权所有 © 云南伯恩科技 证书:粤ICP备09170368号