当前位置: 代码迷 >> VC/MFC >> 学习笔记之深入浅出MFC 第7章 过程与线程(Process and Thread)
  详细解决方案

学习笔记之深入浅出MFC 第7章 过程与线程(Process and Thread)

热度:529   发布时间:2016-05-02 03:12:33.0
学习笔记之深入浅出MFC 第7章 进程与线程(Process and Thread)

OS/2、Windows NT以及Windows 9x都支持多线程,这给程序员带来了很大的便利。然而,在使用多线程的时候,必须要处理好各线程之间的关系,否则会带来很多麻烦。

进程(process)表示一个执行中的程序,线程是CPU的基本的调度单位。

(1)核心对象

对象的概念不知道大家已经理解了没有?所谓的对象,其实就是我们具体做某件事的工具,举个例子,类是属性(共同的特性)的集合,这些集合的具体化(也叫实例化)就是对象。举个例子,我们需要一个类似画笔的工具来画一幅画,那么Windows就给我们提供了这样一个类,这个类包括许多功能(比如画线、彩笔、填充等等),那么怎么用这个类呢?我们先把这个类做成一支笔(外形)----就是对象,然后用对象来画图。现在大家理解了吗?

那么什么是核心对象?像实现一支笔或一把画刷只需要一些函数资源就可以了,但是如果我们如果要使用系统深层的功能呢,那就会用到系统资源,所以能够调用这种深层系统资源的对象我们成为核心对象。系统对象一旦产生,任何应用程序都可以开启并使用该对象。系统会给与核心对象一个计数值作为管理之用。

核心对象包括下列几种:

前三个用于线程的同步化;file-mapping对象用于内存映射文件,process和thread对象则是本节的主角。这些核心对象的产生方式(也就是所使用的API)不同,但是都会获得一个handle作为识别;每次被调用,其对应的计数值就加1。核心对象的结束方式都是一样的,调用CloseHandle即可。

很多人都分不清楚进程跟线程的区别,其实,“进程(process)对象”是一个数据结构,系统用它来管理进程;而线程(Thread)才是用来执行代码的。

(2)一个进程的诞生与死忙

执行一个程序,就必然产生一个进程(process),这就是主进程。最直接也最常用的程序执行方式是在shell(比如资源管理器或文件管理器)中用鼠标双击某一个可执行文件图标(如App.exe),执行起来的App进程其实就是shell调用CreateProcess激活的。

整个的流程如下:

1、shell调用CreateProcess激活App.exe。

2、系统产生一个“进程核心对象”,计数值加1.

3、系统为此进程建立一个4GB的地址空间。

4、加载器将必要的码加载到上述地址空间中,包括App.exe的程序、数据,以及所需要的动态链接库(DLLs)。加载器如何知道要加载哪些DLLs呢?它们被记录在可执行文件(PE档案格式)的.idata section中。

5、系统为此进程建立一个线程,称为主线程(primary thread)。线程才是CPU时间的分配对象。

6、系统调用C runtime函数库的Startup code。

7、Startup code调用App的程序的WinMain函数。

8、App程序开始运行。

9、使用者关闭App主窗口,使WinMain中的消息循环结束掉,于是WinMain结束。

10、回到Startup code。

11、回到系统,系统调用ExitProcess结束行程。

别忘了,我们的程序是通过shell调用激活的,所以可以说,通过这种方式执行起来的所有Windows程序,都是shell的子程序。本来,母进程和子进程之间可以有某些关系存在,但shell在调用CreateProcess时已经把母子之间的脐带关系剪断了,因此事实上它们是独立个体。

(3)产生子进程

可以写一个程序专门用来激活其它的程序。关键就在于你会不会使用CreateProcess。函数如下:



第一个参数lpApplicationName指定可执行文件名。

第二个参数lpCommandName指定预传给新进程的命令行(command line)参数。

如果指定了lpApplicationName,但没有扩展名,系统并不会主动为你加上.exe扩展名;如果没有指定完整的路径(注意:这个函数是用来激活一个已经存在的.exe文件,所以需要指定文件路径),系统就只在当前工作目录中寻找。但如果你指定lpApplicationName为NULL的话,系统会以lpCommandLine的第一个“段落”(其实是术语中所谓的token)作为可执行文件名;如果这个文件名没有指定扩展名,就采用默认的“.exe”扩展名;如果没有指定路径,Windows就依照五个搜寻路径来寻找可执行文件,分别是:

1、调用者的可执行文件所在目录;

2、调用者的当前工作目录;

3、Windows目录;

4、Windows System目录;

5、环境变量中的path所设定的各目录;

我们用下面的实例来解释一下上面的介绍:

CreateProcess("E:\\CWIN95\\NOTEPAD.EXE","README.TXT",...);

系统将执行E:\CWIN95\NOTEPAD.EXE,命令行参数是“README.TXT”。如果我们这样子调用:

CreateProcess(NULL,"NOTEPAD README.TXT",...);

系统将依照搜寻次序,执行第一个被找到的NOTEPAD.EXE(因为命令行参数的第一个“字段”是NOTEPAD,没有后缀,会自动给添加.exe后缀),并传送命令行参数“README.TXT”给它。

建立新进程之前,系统必须做出两个核心对象,也就是“进程对象”和“线程对象”。

第三个参数和第四个参数分别指定这两个核心对象的安全属性。

第五个参数(TRUE或FALSE)则用来设定这些安全属性是否被继承。

第六个参数dwCreationFlags可以是许多常数的组合,会影响到进程的建立过程。这些常数中比较常用的是CREATE_SUSPENDED,它会使得子进程产生之后,其主线程立即被暂停执行。

第七个参数lpEnvironment可以指定进程所使用的环境变量区。通常我们会让子进程继承父进程的环境变量,那么设定为NULL。

第八个参数lpCurrentDirectory用来设定子进程的工作目录与工作驱动器。如果指定为NULL,子进程就会使用父进程的工作目录与工作驱动器。

第九个参数lpStartupInfo是一个指向STARTUPINFO结构的指针。这是一个庞大的结构,可用来设定窗口的标题、位置与大小。

最后一个参数是一个指向_PROCESS_INFORMATION结构的指针:

typedef struct _PROCESS_INFORMATION {

HANDLE hProcess;

HANDLE hThread;

DWORD dwProcessId;

DWORD dwThreadId;

} PROCESS_INFORMATION;

当系统为我们产生“行程对象”和“线程对象”时,它会把两个对象的handle填入此结构的相关字段中,应用程序可以从这里获得这些handles。

如果一个进程想要结束自己的生命,只要调用:

VOID ExitProcess(UINT fuExitCode);就可以了。

如果行程想要结束另一个行程的生命,可以使用:

BOOL TeminateProcess(HANDLE hProcess, UINT fuExitCode);

很显然,只要你有某个行程的handle,就可以结束它的生命。TeminateProcess并不建议使用,倒不是因为权力太大,而是因为一般行程结束时,系统会通知该行程所使用的所有DLLs,但如果以TerminateProcess结束一个行程,系统不会做这件事,这可不是你所希望的。

针对前面所说的“割断脐带”这件事,只要把子行程以CloseHandle关闭,就达到了目的。

PROCESS_INFORMATION ProcInfo;

BOOL fSuccess;\

fSuccess = CreateProcess(...,&ProcInfo);

if(fSuccess) {

CloseHandle(ProcInfo.hThread);

CloseHandle(ProcInfo.hProcess);

}

最后,需要说的是创建进程并不是经常使用,因为我们一般是写一个程序,而一个程序就是一个进程,程序之间的操作并不多,所以我们使用更多的是对一个进程中的多线程的操作。

(4)一个线程的诞生与死亡

进程是为程序开辟了一个存储空间,线程才是真正执行代码的地方。当一个进程建立起来之后,主线程也产生。所以每一个Windows程序一开始就有一个线程。我们可以调用CreateThread产生额外的线程,然后系统会为我们做好如下准备:

1、配置“线程对象”,其handle将成为CreateThread的返回值。

2、设定计数值为1。

3、配置线程的context。

4、保留线程的堆栈。

5、将context中的堆栈指针缓存器(SS)和指令指针缓存器(IP)设定妥当。

所谓的工作切换(context switch)其实就是对线程的context的切换。

想要创建一个新线程,调用CreateThread即可办到:

CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,

DWORD dwStackSize,

LPTHREAD_START_ROUTINE lpStartAddress,

LPVOID lpParameter,

DWORD dwCreationFlags,

LPDWORD lpThreadId

);

第一个参数表示安全属性的设定和继承。

第二个参数设定堆栈的大小。

第三个参数设定“线程函数”名称,而该函数的参数则由这里的第四个参数设定。

第五个参数如果是0,表示让线程立刻开始执行,如果是CREATE_SUSPENDED,则是要求线程暂停执行(我们需要调用ResumeThread才能令其重新开始)。

最后一个参数是一个指向DWORD的指针,存放线程的ID。

当CreateThread成功时,系统为我们把一个线程应该有的东西都准备好了。线程的主体在哪里?就是所谓的线程函数。线程与线程之间,不必考虑控制权释放的问题,以为Win32操作系统的特点是强制性多任务的(也就是说,线程创立之后系统会自动在线程之间切换执行,我们不必控制该哪个线程执行了)。

那么线程创立之后会在操作系统的控制下切换执行,什么时候线程才会结束呢?

线程的结束有两种情况,一种是寿终正寝,一种是未得善终。前者是线程函数正常结束退出,那么线程也就自然而然的结束了。这时候系统会调用ExitThread做些善后清理工作(其实线程中也可以自行调用此函数结束自己)。但是如果线程根本是一个无穷循环,如何结束呢?一种是行程结束(自然也就导致线程结束),二是别的线程强制以TeminateThread将它终结掉。不过,TeminateThread太过毒辣,若非必要还是少用为妙。

(这个地方我觉得很多朋友可能还是有困惑,我在解释一下:线程通过CreateThread创建之后,如果参数选择的立即执行,那么线程就会开始执行了。线程的执行是这样的,比如有两个线程,系统会按每个线程给一段执行时间,两个线程交替执行。线程一创建成功,就会进入这个线程的线程函数,那么每次到了系统分配的执行时间,就会执行线程函数里的代码。如果线程函数里的代码是有限的(非无限循环),那么就肯定会有执行完的时候,那么这个线程也就随着线程函数的执行完成而结束了;如果线程函数是无限循环,也就是说每次到了系统分配的线程执行时间,就开始执行线程函数的循环代码,所以这个线程是永远不会停止的,这时候要想停止就得采用上段介绍的第二种情况了)

还有一点需要说明一下,由于线程创建(CreateThread)之后,就进入了线程函数,所以可以在CreateThread之后接着调用CloseHandle(handle);函数关闭这个线程,这不影响线程函数的执行,也符合对资源的管理原则。

(5)以_beginthreadex取代CreateThread

Windows程序除了调用Win32 API外,通常也难免会调用 C runtime函数。为了保证多线程情况下的安全, C runtime函数库必须为每一个线程做一些登记工作。没有这些记录,C runtime函数库就不知道为每一个线程配置一块新的内存,作为线程的区域变量使用。因此,CreateThread有一个名为_beginThreadex的外包函数,负责额外的登记工作。

_beginThreadex的参数与CreateThread的参数其实完全一样,不过是类型被净化了,不再有Win32类型封装。

_beginThreadex传回的unsigned long事实上就是一个Win32 HANDLE,指向新线程。简单的范例如下:

针对Win32 API ExitThread,也有一个对应的C runtime函数: _endthreadex。

(6) 线程优先级(Priority)

优先级是线程调度的重要依据。优先级越高的线程,肯定会先得到系统地青睐。当然,操作系统也会根据情况调整各个线程的优先级。例如前台线程的优先级应该调高一些,后台线程优先级应该调低一些。

线程优先级范围从0(最低)到31(最高)。指定线程的优先级时,需要两个步骤,第一个步骤是指定进程的“优先级等级”,第二个步骤是给该进程所拥有的线程指定“相对优先级”。优先级等级的设定在CreateProcess的dwCreationFlags参数中指定,如果不指定系统默认的是NORMAL_PRIORITY_CLASS,除非父进程是IDLE_PRIORITY_CLASS。关于等级的描述如下表所示:

idle”等级只有在CPU时间将被浪费掉时(也就是前面讲到的空闲时间)才执行,该等级最适合于系统监视软件,或屏幕保护软件。

“normal”是默认等级。系统可以动态改变优先等级,但只限于“normal”等级。当行程变为前台时,线程优先级提升为9,当行程变为后台时,优先级降低为7.

“high”等级是为了满足立即反应的需要,例如使用者按下Ctrl+Esc时立刻把工作管理器带出场。

“realtime”等级几乎不会被一般的应用程序使用。就连系统中控制鼠标、键盘、驱动器状态重新扫描等线程都比“realtime”优先级低。这种等级使用在“如果不再某一时间范围内被执行的话,数据就要遗失”的情况。使用时一定要慎重,因为可能会导致其它线程无法被执行。

上面的四种等级,是对大范围的设定,当然,你也可以在每一个等级中使用SetThreadPriority设定精确的优先级,并且可以稍高或稍低于该等级的正常值(范围是两个点数)。这个就类似于微操作。


  相关解决方案