WIN32下DELPHI中的多线程【深入VCL源码】

2017-09-05 19:19:23来源:CSDN作者:bedbrother人点击

分享
线程的基础知识      线程的组成线程有两部分组成。     1、一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。     2、另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。     进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。  线程是一种操作系统对象,它表示在进程中代码的一条执行路径。在每一个Wi n32的应用程序中都至少有一个线程,它通常被称为主线程或默认线程。在应用程序中也可以自由地创建别的线程去执行其他任务。线程技术使不同的代码可以同时运行。当然,只有在多C P U的计算机上,多个线程才能够真正地同时运行。在单个CPU上,由于操作系统把C P U的时间分成很短的片段分配给每个线程,这样给人的感觉好像是多个线程真的同时运行,他们只是“看起来”同时在运行。       Win32是一种抢占式操作系统,操作系统负责管理哪个线程在什么时候执行。如果当线程1暂停执行时,线程2才有机会获得C P U时间,我们说线程1是抢占的。如果某个线程的代码陷入死循环,这并不可怕,操作系统仍会安排时间给其他线程。      创建一个线程       注意:每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。线程函数可以使用任何合法的名字。可以给线程函数传递单个参数,参数的含义由你自己定义。线程函数必须由一个返回值,它将成为该线程的退出代码。线程函数应该尽可能的使用函数参数和局部变量。线程函数类似下面的样子(Object Pascal):
上面的的代码功能很简单,你可以在程序中直接调用,例如这样:当你在一个窗口中用这样的方式调用时,你会发现在执行的过程中,你将无法在窗口上进行其他操作,因为它工作于你程序的主线程之中。如果此时,你还希望窗口可以进行其他操作。怎么办?让它在后台工作,让它成为另一个线程,使得不同的代码可以同时运行。    做法很简单,如果想要创建一个或多个辅助线程,只需要让一个已经在运行的线程来调用CreateThread,原型如下:当CreateThread,被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。    下面来说这个函数的几个参数:    1、psa  此参数是指向SECURITY_ATTRIBUTES结构的指针。如果想要该线程内核对象的默认安全属性,可以(并且通常能够)传递NULL。如果希望所有的子进程能够继承该线程对象的句柄,必须设定一个SECURITY_ATTRIBUTES结构,它的bInheritHandle(是否可继承)成员被初始化为True,关于SECURITY_ATTRIBUTES,因为此文的目的不是介绍它,所以这里不做详细介绍,具体可以参考MSDN。通常使用,我们传递null就够了。    2、cbStack用于设定线程可以将多少地址空间用于它自己的堆栈。当调用CrateThread时,如果传递的值不是0,就能使该函数将所有的存储器保留并分配给线程的堆栈。由于所有的存储器预先作了分配,因此可以确保线程拥有指定容量的可用堆栈存储器。通常状况下,我们会设置为0。    3、pfnStartAddr and pvParam,pfnStartAddr 参数用于指明想要新线程执行的线程函数的地址。线程函数的pvParam参数与原先传递给CreateThread的pvParam参数是相同的。CreateThread使用该参数不做别的事情,只是在线程启动执行时将该参数传递给线程函数。该参数提供了一个将初始化值传递给线程函数的手段。该初始化数据既可以是数字值,也可以是指向包含其他信息的一个数据结构的指针。此时回头再去看我上面例子上的MyThread,你会发现它由一个无类型的指针参数(用C来描述,应该是PVOID),在创建线程时,这个参数就通过pvParam来赋值。    4、fdwcreate 此参数可以设定用于控制创建线程的其他标志。它可以是两个值中的一个。如果该值是0,那么线程创建后可以立即进行调度。如果该值是CREATE_ SUSPENDED,系统可以完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。在DELPHI的WINDOWS.PAS单元,你可以发现它的定义CREATE_SUSPENDED= $00000004;    5、pdwThreadId 最后一个参数必须是Dword的一个有效地址,CreateThread使用这个地址来存放系统分配给新线程的ID.       有了上面这些基础,下面我们就使用createThread来创建刚才那个MyThread线程(DELPHI7);执行此段代码,你会发现,它依然会在屏幕指定区域输出文字,和最开始时我们用把MyThread在主线程中运行不同的是,此时,你依然可以对窗口进行其他操作。     看代码的最后一行,它使用了createThread,看它的参数,第一个nil以及第二个0意外着,它使用默认的安全设置以及默认的线程堆栈大小,第三个参数是MyThread的地址(注意@符号),然后我们传递了ppi这个Pinfo类型的指针,使得线程函数接受一个参数,如果你不准备让线程接受这个参数,用nil,fdwcreate参数,我们赋值为0,意味着我们希望线程立即执行,最后一个参数用来接受新线程的ID。让我们来看看CreateThread都干了些什么。上图显示了系统在创建线程和对线程进行初始化时必须做些什么工作。调用CreateThread可使系统创建一个线程内核对象。该对象的初始使用计数是2(在线程停止运行和从CreateThread返回的句柄关闭之前,线程内核对象不会被撤消)。线程的内核对象的其他属性也被初始化,暂停计数被设置为1,退出代码始终为STILL_ACTIVE(0 x 1 0 3),该对象设置为未通知状态。        一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间分配而来的,因为线程并不拥有它自己的地址空间。然后系统将两个值写入新线程的堆栈的上端(线程堆栈总是从内存的高地址向低地址建立)。写入堆栈的第一个值是传递给CreateThread的pvParam参数的值。紧靠它的下面是传递给CreateThread的pfnStartAddr参数的值。每个线程都有它自己的一组C P U寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的CPU寄存器的状态。线程的这组C P U寄存器保存在一个CONTEXT结构。CONTEXT结构本身则包含在线程的内核对象中。    指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。当线程的内核对象被初始化时,CONTEXT结构的堆栈指针寄存器被设置为线程堆栈上用来放置pfnStartAddr的地址。当线程完全初始化后,系统就要查看CREATE_SUSPENDED标志是否已经传递给CreateThread。如果该标志没有传递,系统便将线程的暂停计数递减为0,该线程可以调度到一个进程中。然后系统用上次保存在线程上下文中的值加载到实际的C P U寄存器中。这时线程就可以执行代码,并对它的进程的地址空间中的数据进行操作。    在这里,我还要简单的描述一下CONTEXT结构,因为WIN32是抢占式操作系统,一个线程几乎不可能永远的占据CPU,也就是说,它会在一定时间后(在WINDOWS中,大概式20ms的时间),被CPU放在一边,一段时间之后,才可以重新获得CPU时间片,此时就有一个问题,线程现在执行到了那里,CPU在再次分配给它时间片执行的时候,必须知道这些信息,难道要从0开始吗?CONTEXT结构的作用就是用来解决这个问题。    在Platform SDK中,你可以看到下面的信息:    “CONTEXT结构包含了特定处理器的寄存器数据。系统使用CONTEXT结构执行各种内部操作。目前,已经存在为Intel、MIPS、Alpha和PowerPC处理器定义的CONTEXT结构。若要了解这些结构的定义,参见头文件WinNT.h”。       该文档并没有说明该结构的成员,也没有描述这些成员是谁,因为这些成员要取决于Windows在哪个CPU上运行。实际上,在Windows定义的所有数据结构中,CONTEXT结构是特定于CPU的唯一数据结构。那么CONTEXT结构中究竟存在哪些东西呢?它包含了主机C P U上的每个寄存器的数据结构。在x86计算机上,数据成员是Eax、Ebx、Ecx、Edx等等。如果是Alpha处理器,那么数据成员包括IntV0、IntT0、IntT1、IntS0、In tRa和IntZero等等。        Windows实际上允许查看线程内核对象的内部情况,以便抓取它当前的一组CPU寄存器。若要进行这项操作,只需要调用GetThreadContext函数。关于此函数的使用,我们下次再说。线程的终止   终止一个线程的运行,有4个方法:   1、线程函数返回,这是最好的   2、调用ExitThread函数,线程将自动撤销   3、调用TerminateThread函数   4、包含线程的进程终止运行线程函数返回  始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。如果线程能够返回,就可以确保下列事项的实现:• 在线程函数中创建的所有C + +对象均将通过它们的撤消函数正确地撤消。• 操作系统将正确地释放线程堆栈使用的内存。• 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。• 系统将递减线程内核对象的使用计数。调用Exitthread函数  void ExitThread(DWORD dwExitCode);  该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是程序中用到的资源(例如DELPHI类对象)将不被撤消。调用TerminateThread函数  Bool TerminateThread(HANDLE hThread,DWORD dwExitCode);    关产这个函数和ExitThread的区别,你会发现它除了有dwExitCode这个退出码参数之外,还包含了可指定线程的句柄参数。看到这里你就应该会想到两者的区别,Exitthread总是撤消调用的线程,而TerminateThread能够撤消任何线程。hThread参数用于标识被终止运行的线程的句柄。当线程终止运行时,它的退出代码成为你作为dwExitCode参数传递的值。同时,线程的内核对象的使用计数也被递减。值得注意的是,此函数是异步运行的函数,也就是说,它告诉系统你想要线程终止运行,但是,当函数返回时,不能保证线程被撤消。如果需要确切地知道该线程已经终止运行,必须调用WaitForSingleObject或者类似的函数,传递线程的句柄。在进程终止时撤销线程   这是很容易想到的。无须过多解释。线程终止时发生的操作当线程终止运行时,会发生下列操作:• 线程拥有的所有用户对象均被释放。在Windows中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。• 线程的退出代码从STILL_ACTIVE改为传递给ExitThread或TerminateThread的代码• 线程内核对象的状态变为已通知。• 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。• 线程内核对象的使用计数递减1。当一个线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。     一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调用GetExitcodeThread来检查由hThread标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码.   BOOL GetExitcodeThread(HANDLE hThread,PDWORD pdwExitcode);   退出代码的值在pdwExitcode);指向的DWORD中返回。如果调用GetExitcodeThread时线程尚未终止运行,该函数就用STILL_ACTIVE标识符(定义为0x103)填入DWORD。如果该函数运行成功,便返回T R U E。       上面描述了结束线程的多种办法,这里必须说明一点,如果有可能,那尽量使用第一种方式来结束线程,它可以确保你释放了所有的资源。好的程序应该尽可能的减少对客户资源的浪费。stdcall       准确的说,stdcall这个标示符本来和线程没有直接的联系,但因为我这里的示例代码是用Object Pascal写的,而我们调用的CreateThread则是用c实现的,这两种语言的函数入栈的方式是不同的,pascal是从左到右。加上stdcall,可以使得入栈方式改为从右到左以符合别的语言的习惯。我们上面调用createThread函数时,因为我传递了那个无类型的指针参数,所以,必须加上stdcall指明入栈方式,否则会出现地址访问错误。当然,如果你并不决定传递参数,你也可以不使用stdcall。不过作为一种好的编码习惯,你最好还是加上。DELPHI中创建线程        如果你只想做一个代码搬运工,你完全可以不了解上面的内容,但如果你想成为一个合格的WIN32程序员,深入这些内容,比你肤浅的多学一门语言有用。       DELPHI把有关线程的API封装在TThread这个Object Pascal的对象中。结合上面的内容,先去看TThread源码从TThread的声明中可以看出,它定义了Windows和Linux下分别要完成的操作,这里我们只谈WIN32,TThread直接从TObject继承,因为,它不是组件。你还可以看到它有一个Execute的方法        procedure Execute; virtual; abstract;      并且你可以看到,它是抽象的,因为,不能创建TThread的实例,你只能创建它的派生类的实例。再去看看它的构造函数,你会看到这样一句代码    FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);再深入去看这个BeginThread,    Result := CreateThread(SecurityAttributes, StackSize, @ThreadWrapper, P,CreationFlags, ThreadID);你看到了什么?是的,CreateThread,结合这两句,看看它都干了些什么,默认的安全属性,默认的堆栈大小,一个入口地址,一个参数,一个创建标志,还有一个threadid。你和本文最开始的那些内容对上了吗?我们又看到它传递的线程函数是ThreadProc,再去看看它。下面只帖了一些和本文有关系的代码它首先根据TThread类中的一个属性Terminated(布尔类型)来判断线程的状态,如果你没有通过外部代码将Terminated甚至为true,它将会执行Execute(注意这个方法,我们刚才提到过它是一个抽象的,你必须让它干点什么,也就是说,Tthread.execute将是你的线程将要执行的操作)。然后是异常的处理。你是否对DELPHI的TThread有点了解了呢?如果有兴趣,好好看看它的源码吧。        说到这里,DELPHI中TThread创建一个线程的基本流程就出来了。调用自己的构造函数,传递一个布尔类型的变量,这个变量对应CreateThread函数的fdwcreate参数,用来决定线程是立即执行还是挂起,构造函数又调用了一个BeginThread,而正是这个BeginThread调用了WIN API Cre

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台