12.2 多线程编程

admin 2022年5月1日13:30:39评论20 views字数 15020阅读50分4秒阅读模式

12.2.1  一个单线程的“问题程序”

来看一个“问题程序”,假设编写一个计数程序,程序的要求如下:

●   界面如图12.1所示,用对话框做主界面,对话框中放置“计数”按钮和“暂停/恢复”按钮,并有一个编辑框用来显示计数结果。

●   开始计数之前,“暂停/恢复”按钮处于灰化状态,当开始计数后,该按钮被激活,用户按动一次这个按钮,则计数暂停,再一次按动,则继续计数。

●   开始计数后,“计数”按钮上的文字将被改为“停止计数”,如果按下“停止计数”按钮,程序将恢复初始状态——第一个按钮变回“计数”按钮,同时灰化“暂停/恢复”按钮。

12.2 多线程编程


程序的代码在所附光盘的Chapter12Counter目录中,里面的Counter.rc文件定义了图12.1所示的对话框,其代码如下:


//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#include                <resource.h>//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#define ICO_MAIN                    1000#define DLG_MAIN                    1000#define IDC_COUNTER             1001#define IDC_PAUSE               1002//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN        ICON                "Main.ico"//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>DLG_MAIN DIALOG 227, 187, 129, 48STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "“问题程序”——计数器"FONT 9, "宋体"{ LTEXT "计数值:", -1, 10, 10, 34, 8 EDITTEXT IDC_COUNTER, 47, 8, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP PUSHBUTTON "计数", IDOK, 8, 27, 50, 14 PUSHBUTTON "暂停/恢复", IDC_PAUSE, 68, 27, 50, 14, WS_DISABLED | WS_TABSTOP}//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

汇编源文件写起来似乎很简单,只要在WM_COMMAND消息中对“计数”和“暂停/恢复”按钮的动作进行处理就可以了。在“计数”按钮中,可以调用一个计数子程序不停地进行加法运算并将结果显示出来,为了能够随时停止或暂停,可以设置一个标志位,按动“停止计数”或者“暂停/恢复”按钮时设置不同的标志,计数子程序在循环中通过测试这个标志位来决定是否暂停或退出。按照这个思路,程序可以写成目录中Counter.asm所示的样子:

   

                 .386                    .model flat, stdcall                    option casemap :none;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Include 文件定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>include         windows.incinclude         user32.incincludelib      user32.libinclude         kernel32.incincludelib      kernel32.lib;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Equ 等值定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN            equ     1000DLG_MAIN            equ     1000IDC_COUNTER     equ     1001IDC_PAUSE       equ     1002;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 数据段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                    .data?hInstance       dd      ?hWinMain            dd      ?hWinCount       dd      ?hWinPause       dd      ? dwOption            dd      ?           ;标志位F_PAUSE         equ     0001h       ;暂停标志F_STOP          equ     0002h       ;停止标志F_COUNTING      equ     0004h       ;计算中标志                    .constszStop          db      '停止计数',0szStart         db      '计数',0;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 代码段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                    .code;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>_Counter            proc                     or      dwOption,F_COUNTING                    and     dwOption,not (F_STOP or F_PAUSE)                    invoke  SetWindowText,hWinCount,addr szStop                    invoke  EnableWindow,hWinPause,TRUE                     xor ebx,ebx                    .while  ! (dwOption & F_STOP)                            .if     !(dwOption & F_PAUSE)                                    inc     ebx                                    invoke  SetDlgItemInt,hWinMain,                                            IDC_COUNTER,ebx,FALSE                            .endif                    .endw                     invoke  SetWindowText,hWinCount,addr szStart                    invoke  EnableWindow,hWinPause,FALSE                    and     dwOption,not (F_COUNTING or F_STOP or F_PAUSE)                    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                                    or      dwOption,F_STOP                            .else                                    call    _Counter                            .endif                    .elseif ax ==   IDC_PAUSE                            xor dwOption,F_PAUSE                    .endif;********************************************************************                .elseif eax ==  WM_CLOSE                        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;********************************************************************                .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

这个程序很简单:当用户按下“计数”按钮的时候,WM_COMMAND消息处理代码调用_Counter子程序进行计数,子程序会将IDOK按钮上的文字通过SetWindowText函数改为“停止计数”,并且使用EnableWindow函数激活“暂停/恢复”按钮,然后进入计数循环。

在循环中,程序通过dwOption变量中的第1位(预定义为F_STOP)来判断是否停止,通过第0位(预定义为F_PAUSE)来决定是否暂停计数。这些标志位的状态以后会在按下“停止计数”或“暂停/恢复”按钮时在WM_COMMAND消息中设置。

粗看起来,程序天衣无缝,现在运行一下看看——“计数”按钮被正确地改为“停止计数”,“暂停/恢复”按钮也正确地被激活了,但是接下来就不对了,编辑框中并没有显示计数值,更糟糕的是接下来所有的按钮都无法按动,对话框也无法移动和无法关闭,总之,程序停止了响应,现在能结束它的惟一办法是通过任务管理器强制结束!

为什么会这样呢?这是因为主线程自从开始进入计数循环以后,就一直在那里“埋头苦干”,忙于计数工作,以至于把WM_COMMAND消息的处理抛到脑后去了,WM_COMMAND消息没有返回,对话框内部的消息循环就停留在DispatchMessage函数里面,以至于消息队列中的后续消息堆积在那里无法处理,这样不管用户按动“停止计数”按钮也好,移动对话框也好,这些动作虽然会被Windows检测到并转换成相应的消息放入消息循环中去,但是这些消息堆积在那里无法处理,所以就看不到对话框有任何的响应。

程序进入了一个怪圈:停止或暂停循环的条件是设置标志位,标志位是在按动“停止计数”或“暂停/恢复”按钮的WM_COMMAND消息中设置的,而WM_COMMAND消息被堆积在消息队列中无法处理,结果标志位永远不可能被设置,程序也就永远无法动弹了。虽然在程序一动不动的背后计数工作还在进行,显示计数值的SetDlgItemInt函数也不停地被调用,但是刷新对话框的WM_PAINT消息也同样没有被处理,所以编辑框中的计数值也无法被显示出来。

这个“问题程序”是Win32编程中“1/10秒规则”的一个极端例子,1/10秒规则指窗口过程处理任何一条消息的时间都不应该超过1/10秒,否则会因为消息的堆积造成程序对用户动作的响应变得迟缓。如果一条消息的处理时间超过1/10秒,那么就最好采取别的方法来完成,第4章中介绍的在消息循环中使用PeekMessage来获取空闲时间的方法就是一种,另一种方法是使用定时器在指定的时间间隔中每次完成一小部分工作,但对于这两种方法,程序必须将一个长时间的工作划分成多个小的部分,每部分的操作时间应该少于1/10秒。

显然,这两种方法也不是很好的办法,因为在不同主频的计算机中,1/10秒时间内可以处理的工作量是不同的,如果按照300 MHz处理器设计每小部分工作的工作量,那么到1GHz处理器上运行时,空出来的时间就被浪费了。实际上,解决1/10秒问题的最好办法就是使用多线程编程技术,程序可以建立一个新的线程来完成时间可能超过1/10秒的工作。

12.2.2  多线程的解决方法

1. 改进后的程序

对于这个“问题程序”,如果让_Counter子程序在一个新的线程中执行,那么在WM_COMMAND消息的处理中,需要做的工作就仅是启动一个新的线程而已,线程启动后,窗口过程就可以马上返回,消息队列中的消息就可以继续得到处理了。与此同时,_Counter子程序则会在另一个线程中继续运行。

说得多不如做得多,现在用多线程的方法来改进前面的Counter程序,修改后的源代码在所附光盘的Chapter12Thread目录中,其中Counter.rc文件的内容保持不变。汇编源程序Counter.asm则改为如下代码:

   

                 .386                    .model flat, stdcall                    option casemap :none;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Include 文件定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>include         windows.incinclude         user32.incincludelib      user32.libinclude         kernel32.incincludelib      kernel32.lib;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; Equ 等值定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN            equ     1000DLG_MAIN            equ     1000IDC_COUNTER     equ     1001IDC_PAUSE       equ     1002;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 数据段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                    .data?hInstance       dd      ?hWinMain            dd      ?hWinCount       dd      ?hWinPause       dd      ? dwOption            dd      ?F_PAUSE         equ     0001hF_STOP          equ     0002hF_COUNTING      equ     0004h                    .constszStop          db      '停止计数',0szStart         db      '计数',0;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; 代码段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>                    .code;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>_Counter            proc        uses ebx esi edi,_lParam        ;经过修改之处                     or      dwOption,F_COUNTING                    and     dwOption,not (F_STOP or F_PAUSE)                    invoke  SetWindowText,hWinCount,addr szStop                    invoke  EnableWindow,hWinPause,TRUE                     xor     ebx,ebx                    .while  ! (dwOption & F_STOP)                            .if     !(dwOption & F_PAUSE)                                    inc     ebx                                    invoke  SetDlgItemInt,hWinMain,                                            IDC_COUNTER,ebx,FALSE                            .endif                    .endw                     invoke  SetWindowText,hWinCount,addr szStart                    invoke  EnableWindow,hWinPause,FALSE                    and     dwOption,not (F_COUNTING or F_STOP or F_PAUSE)                    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                                        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                            .endif;********************************************************************                    .elseif eax ==  WM_CLOSE                            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;********************************************************************                .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


将修改后的源程序和修改前的对比一下,可以发现不同的只有两个地方:第一是处理“计数”按钮的WM_COMMAND消息中,调用_Counter子程序的指令变成了对CreateThread函数的调用,这个函数就是用来创建新线程的函数;第二是_Counter子程序的定义有点不同,这是因为用做线程入口的线程函数必须按照规定的格式定义。这两个不同点其实也可以归结为一个,因为第二个不同点实际上是对第一个不同点的配合。

是不是这样修改后程序就正常工作了呢?可以来验证一下:运行程序,按下“计数”按钮,这次在“计数”按钮被改为“停止计数”,“暂停/恢复”按钮被激活的同时,计数值可以在编辑框中显示出来了,而且在计数的过程中,可以移动程序位置、关闭程序、按动各个按钮——总之,计数线程和主线程在同时工作了。

接下来参考这个程序来探讨多线程程序的结构。

2. 多线程程序的结构

Windows中存在很多类型的对象:如窗口类、窗口、文件、菜单、图标、光标和钩子等,当一个线程创建某个对象的时候,这个对象归线程所属的进程所拥有,进程中的其他线程也可以使用它们,比如可以在主线程中打开一个文件,然后在另一个子线程中读写这个文件。

大部分类型的对象不属于创建它的线程,而是属于进程,这表现在创建对象的线程结束时,如果线程不去主动删除这些对象,系统不会自动删除它们,只有当整个进程结束时对象还没有被删除,系统才会自动删除它们。但是窗口和钩子这两种对象比较特殊,它们首先是由创建窗口和安装钩子的线程所拥有的,如果一个线程创建一个窗口或安装一个钩子,然后线程结束,那么系统会自动摧毁窗口或卸载钩子。

进程中的消息队列则与线程和窗口都是相关的,如果在一个线程中创建了一个窗口,那么Windows就会单独给这个线程分配一个消息队列,为了让这个窗口工作正常,线程中就必须存在一个消息循环来派送消息,这就是主线程中只有当创建窗口时才需要消息循环代码,不创建窗口的程序(如控制台程序)就不需要消息循环的原因。这也就意味着如果窗口是在子线程中创建的,主线程中的消息循环根本不会获得这个窗口的消息,子线程必须自己设置一个消息循环。当使用SendMessage或者PostMessage函数向一个窗口发送消息的时候,系统会先判别窗口是被哪个线程创建的,然后把消息派送到正确线程的消息队列中。

整理一下思路:如果在一个线程中创建窗口,就必须设置消息循环,有了消息循环,就必须遵循1/10秒规则,这就意味着这个线程不该用来处理长时间的工作。而在一个程序中为不同的线程设置多个消息循环,不但使代码复杂化,而且会产生诸多的其他问题,所以在多线程程序中,规划好程序的结构是很重要的。

规划多线程程序的原则就是:首先,处理用户界面(指拥有窗口和需要处理窗口消息)的线程不该处理1/10秒以上的工作;其次,处理长时间工作的线程不该拥有用户界面。根据这个规则,我们大致可以把线程分成两大类:

●   处理用户界面的线程——这些线程创建窗口并设置消息循环来负责处理窗口消息,一个进程中并不需要太多这种线程,一般让主线程负责这个工作就可以了。

●   工作线程——该类线程不处理窗口界面,当然也就不用处理消息了,它一般在后台运行,干一些繁重的需要长时间运行的粗活。

一般来说,处理用户界面的线程交给主线程来做就可以了。如果主线程中接到一个用户指令,完成这个指令可能需要比较长的时间,主线程可以建立一个工作线程来完成这个工作,并负责指挥这个工作线程。这和我们日常生活中的许多例子是很像的,比如,公司的经理就好比是用户界面线程,他负责和外界沟通,谈判业务,对董事会(指对着屏幕单击鼠标的用户)汇报,同时负责将具体的工作分配给职能部门(也就是工作线程)做,如果让经理具体地去做每一件事情,下车间去包装产品或开着卡车外出拉原料,那么他就无法管理好这个公司了。

在多线程版本的Counter.asm例子中,使用的就是这样的程序结构:主线程用来维护界面,接收用户的输入动作并安排相应的操作,工作线程则用来进行计数操作。

3. 线程之间的通信

主线程在创建工作线程的时候,可以通过参数给工作线程传递初始化数据,当工作线程开始运行后,还需要通过通信机制来控制工作线程,就像经理虽然不用亲自干活,也需要随时了解和控制情况一样;同样,工作线程有时候也需要将一些情况主动通知主线程。

线程之间的通信可以归纳为3种方法。

使用全局变量传递数据是最常用的方法,如例子中主线程通过设置全局变量dwOption中的数据位来通知工作线程,工作线程随时检查这个变量并根据要求做相应的动作;反过来,工作线程也通过设置dwOption的第2位(预定义为F_COUNTING)来控制主线程中对IDOK按钮的动作,如果F_COUNTING被置位,表示线程在运行中,这时IDOK按钮被定义为“停止计数”按钮,否则IDOK按钮被定义为“计数”按钮。使用全局变量传递数据的缺点是当


多个工作线程使用同一个全局变量时,可能会引起同步问题,在12.4节中会探讨这个问题。

第2种方法是通过发送消息来通信,如工作线程工作结束时,可以通过向主线程发送自定义的WM_XXX消息来通知主线程,这样主线程就不需要随时去检查工作线程是否结束。只要在窗口过程中处理WM_XXX消息就可以了。这种方法的缺点是无法向工作线程发送消息,因为工作线程中一般并没有消息队列,所以这种方法仅用在工作线程向主线程传递消息的应用中。

如果线程之间传递的不是数据而是代表状态的布尔值,也可以使用第3种方法,即使用事件对象来通信,相关内容会在12.3节中详细介绍。

12.2.3  与线程有关的函数

1. 创建线程

创建一个线程可以使用CreateThread函数,函数的用法是:

    invoke  CreateThread,lpThreadAttributes,dwStackSize,lpStartAddress,            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 多线程编程


原文始发于微信公众号(汇编语言):12.2 多线程编程

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月1日13:30:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   12.2 多线程编程https://cn-sec.com/archives/966825.html

发表评论

匿名网友 填写信息