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设定精确的优先级,并且可以稍高或稍低于该等级的正常值(范围是两个点数)。这个就类似于微操作。