线程的基础知识
线程的组成。线程有两部分组成。
1、一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
2、另一个是线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。
进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
线程是一种操作系统对象,它表示在进程中代码的一条执行路径。在每一个Wi n32的应用程序中都至少有一个线程,它通常被称为主线程或默认线程。在应用程序中也可以自由地创建别的线程去执行其他任务。线程技术使不同的代码可以同时运行。当然,只有在多C P U的计算机上,多个线程才能够真正地同时运行。在单个CPU上,由于操作系统把C P U的时间分成很短的片段分配给每个线程,这样给人的感觉好像是多个线程真的同时运行,他们只是“看起来”同时在运行。
Win32是一种抢占式操作系统,操作系统负责管理哪个线程在什么时候执行。如果当线程1暂停执行时,线程2才有机会获得C P U时间,我们说线程1是抢占的。如果某个线程的代码陷入死循环,这并不可怕,操作系统仍会安排时间给其他线程。
创建一个线程
注意:每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。线程函数可以使用任何合法的名字。可以给线程函数传递单个参数,参数的含义由你自己定义。线程函数必须由一个返回值,它将成为该线程的退出代码。线程函数应该尽可能的使用函数参数和局部变量。线程函数类似下面的样子(Object Pascal):
//注意最后的stdcall,后面我会描述一些有用的东西
function MyThread(info : Pointer):DWORD; stdcall;
var
i : integer;
begin
for i := 0 to Pinfo(info)^.count-1 do
Form1.Canvas.TextOut(Pinfo(info)^.x,Pinfo(info)^.y,inttostr(i));
Result := 0;
end;
上面的的代码功能很简单,你可以在程序中直接调用,例如这样:
type
Tinfo = record
count : integer;
x : integer;
y : integer;
end;
Pinfo= ^Tinfo;
...
procedure TForm1.Button4Click(Sender: TObject);
var
ppi : Pinfo;
begin
ppi :=AllocMem(sizeof(tinfo));
ppi^.count := 1000000;
ppi^.x := 100;
ppi^.y := 400;
MyThread(ppi);
end;
当你在一个窗口中用这样的方式调用时,你会发现在执行的过程中,你将无法在窗口上进行其他操作,因为它工作于你程序的主线程之中。如果此时,你还希望窗口可以进行其他操作。怎么办?让它在后台工作,让它成为另一个线程,使得不同的代码可以同时运行。
做法很简单,如果想要创建一个或多个辅助线程,只需要让一个已经在运行的线程来调用CreateThread,原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
当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);
...
//一个自定义类型
type
Tinfo = record
count : integer;//计数器个数
x : integer;//要显示在窗体上位置的横座标
y : integer;//纵坐标
end;
Pinfo=^Tinfo;
var
MyThreadHad : THandle;//一个全局变量,用来接受CreateThread创建新线程的句柄
...
procedure TForm1.Button4Click(Sender: TObject);
var
ppi : Pinfo;
MyThreadId : DWORD;
begin
...{分配空间,注意,因为这里我只是一个用来演示CreateThread使用的代码,所以没有释放pp,但优秀的代码最后记得分配了空间一定要释放}
ppi :=AllocMem(sizeof(tinfo));
//初始化
ppi^.count := 100000;
ppi^.x := 100;
ppi^.y := 400;
//下面这行代码是关键
MyThreadHad := CreateThread(nil,0,@MyThread,ppi,0,MyThreadId);
end;
执行此段代码,你会发现,它依然会在屏幕指定区域输出文字,和最开始时我们用把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 = class
private
...{$IFDEF MSWINDOWS}
FHandle: THandle;
FThreadID: THandle;
...{$ENDIF}
...{$IFDEF LINUX}
// ** FThreadID is not THandle in Linux **
FThreadID: Cardinal;
FCreateSuspendedSem: TSemaphore;
FInitialSuspendDone: Boolean;
...{$ENDIF}
FCreateSuspended: Boolean;
FTerminated: Boolean;
FSuspended: Boolean;
FFreeOnTerminate: Boolean;
FFinished: Boolean;
FReturnValue: Integer;
FOnTerminate: TNotifyEvent;
FSynchronize: TSynchronizeRecord;
FFatalException: TObject;
procedure CallOnTerminate;
class procedure Synchronize(ASyncRec: PSynchronizeRecord); overload;
...{$IFDEF MSWINDOWS}
function GetPriority: TThreadPriority;
procedure SetPriority(Value: TThreadPriority);
...{$ENDIF}
...{$IFDEF LINUX}
// ** Priority is an Integer value in Linux
function GetPriority: Integer;
procedure SetPriority(Value: Integer);
function GetPolicy: Integer;
procedure SetPolicy(Value: Integer);
...{$ENDIF}
procedure SetSuspended(Value: Boolean);
protected
procedure CheckThreadError(ErrCode: Integer); overload;
procedure CheckThreadError(Success: Boolean); overload;
procedure DoTerminate; virtual;
procedure Execute; virtual; abstract;
procedure Synchronize(Method: TThreadMethod); overload;
property ReturnValue: Integer read FReturnValue write FReturnValue;
property Terminated: Boolean read FTerminated;
public
constructor Create(CreateSuspended: Boolean);
destructor Destroy; override;
procedure AfterConstruction; override;
procedure Resume;
procedure Suspend;
procedure Terminate;
function WaitFor: LongWord;
class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload;
class procedure StaticSynchronize(AThread: TThread; AMethod: TThreadMethod);
property FatalException: TObject read FFatalException;
property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
...{$IFDEF MSWINDOWS}
property Handle: THandle read FHandle;
property Priority: TThreadPriority read GetPriority write SetPriority;
...{$ENDIF}
...{$IFDEF LINUX}
// ** Priority is an Integer **
property Priority: Integer read GetPriority write SetPriority;
property Policy: Integer read GetPolicy write SetPolicy;
...{$ENDIF}
property Suspended: Boolean read FSuspended write SetSuspended;
...{$IFDEF MSWINDOWS}
property ThreadID: THandle read FThreadID;
...{$ENDIF}
...{$IFDEF LINUX}
// ** ThreadId is Cardinal **
property ThreadID: Cardinal read FThreadID;
...{$ENDIF}
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end;
从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,再去看看它。下面只帖了一些和本文有关系的代码
try
if not Thread.Terminated then
try
Thread.Execute;
except
Thread.FFatalException := AcquireExceptionObject;
end;
finally
它首先根据TThread类中的一个属性Terminated(布尔类型)来判断线程的状态,如果你没有通过外部代码将Terminated甚至为true,它将会执行Execute(注意这个方法,我们刚才提到过它是一个抽象的,你必须让它干点什么,也就是说,Tthread.execute将是你的线程将要执行的操作)。然后是异常的处理。你是否对DELPHI的TThread有点了解了呢?如果有兴趣,好好看看它的源码吧。
说到这里,DELPHI中TThread创建一个线程的基本流程就出来了。调用自己的构造函数,传递一个布尔类型的变量,这个变量对应CreateThread函数的fdwcreate参数,用来决定线程是立即执行还是挂起,构造函数又调用了一个BeginThread,而正是这个BeginThread调用了WIN API CreateThread,它将一个ThreadProc线程函数传递给CreateThread,而这个ThreadProc则调用你必须覆盖的方法Execute来完成你想要进行的操作。
再来看看它的终止,继续刚才的内容,看ThreadProc这个函数的下面代码,你会发现,当Execute执行完毕之后,它就认为这个线程终止了,它调用了EndThread(Result),然后这个EndThread又调用了ExitThread(ExitCode)。当结束使用TThread对象时,应该确保已经把这个Object Pascal对象从内存中清除了。这才能确保所有内存占有都释放掉。尽管在进程终止时会自动清除所有的线程对象,但及时清除已不再用的对象,可以使内存的使用效率提高。还是ThreadProc的源码,你会发现当线程的Execute执行完之后,它要根Thread.FFreeOnTerminate来决定是否释放资源。FreeThread := Thread.FFreeOnTerminate;...if FreeThread then Thread.Free;这是非常好的,也就是说,你可以通过在对FreeOnTerminate这个属性赋值为true(观察它的源码,FreeOnTerminate是FFreeOnTerminate这个私有变量的访问器),来让TThread对象自动在线程执行完毕之后自动释放资源。
看了这么多,我们可以梳理一下思路了,使用TThread对象,我们必须从它派生一个类,然后你必须覆盖Execute这个方法,在这里,完成你要让线程做的事情。如果有可能(或者说尽量,除非你对这个线程还有别的需求),还可以在这里通过设置FreeOnTerminate := true,使得线程在执行完毕之后自动释放资源。我们可以通过TThread对象构造函数的参数来决定线程是否立即运行。
一个例子:
...
//声明一个线程,我们叫它TFrist
Tfrist = class(TThread)
protected
procedure Execute;override;//覆盖Execute这个抽象的方法,这是你必须做的事情
end;
var
Form1: TForm1;
Ci : array[0..1000] of integer;//一个全局变量,我们将用TFrist来访问它
...
...{ Tfrist }
procedure Tfrist.Execute;
var
i : integer;
begin
inherited;
OnTerminate := Form1.ThreadDone;//注意一下这里
FreeOnTerminate := true;
for i := 0 to 1000 do
ci := i;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
//初始化全局变量
FillMemory(@ci,1000,0);
Tfrist.Create(false);
end;
procedure TForm1.ThreadDone(sender: TObject);
var
i : integer;
begin
for I := 0 to 1000 do
ListBox1.Items.Add(IntToStr(ci))
end;
上面我省略了一些代码,但大意已表。我们声明了一个TFrist的类,它从TThread继承而来,它将对一个全局变量的的数组CI进行初始化,并且将初始化的结果显示在窗体的ListBox1上。
写到这里,你会发现上述代码中的几个“疑点”,其中一个我现在要说明的就是OnTerminate := Form1.ThreadDone;这一句,观察ThreadDone的源码,你会发现它其实就是完成将全局变量的内容显示在窗体的LISTBOX中,这时,你可能会问,直接写在线程里,不可以吗?为什么要这样?原因很简单。大多数V C L在被设计时,都只考虑了在任何时刻只有一个线程来访问它。其局限性尤其体现在V C L的用户界面部分。同时,一些非用户界面部分也不是线程安全的。
1. 非用户界面的V C L
实际上V C L只有很少的部分保证是线程安全的。可能在这很少的部分中,最让人注意的是V C L的属性流机制。V C L的流机制确保了组件流能被多线程安全地读写。请记住即使最基础的V C L类(诸如TList),也不是为安全地同时操作多个线程而设计的。对某些情况, V C L提供了一些线程安全的替代,比如,用TThreadList 来替代TList可以解决多个线程操作的问题。
2. 用户界面的V C L
V C L要求所有的用户界面控制要发生在一个应用程序的主线程的环境中(线程安全的TCanvas类除外)。当然,利用技术手段是可以有效地利用附属线程更新用户界面的(后面将会讨论)。
对V C L的访问只能在主线程中。这将意味着:所有需要与用户打交道的代码都只能在主线程的环境中执行。这是其结构上明显的不足,并且这种需求看起来只局限在表面上,但它实际上有一些优点。首先,只有一个线程能够访问用户界面,这减少了编程的复杂性。Win32要求每个创建窗口的线程都要使用GetMessage()建立自己的消息循环。正如你所想的,这样的程序将会非常难于调试,因为消息的来源实在太多了。其次,由于V C L只用一个线程来访问它,那些用于把线程同步的代码就可以省略了,从而改善了应用程序的性能。
那么,如果有多个线程要访问VCL,怎么办呢?有这么几个方法:
1、利用TThread的OnTerminate属性,它是一个TNofityEvent类型,它指定的过程将在线程执行完毕之后运行,并且是运行在主线程环境中的,我上面的代码就是使用了这种方法。
线程的调度
每个线程是拥有一个上下文结构的,这个结构维护在线程的内核对象中。这个上下文结构反映了线程上次运行时该线程的C P U寄存器的状态。每隔20ms左右,Windows要查看当前存在的所有线程内核对象。在这些对象中,只有某些对象被视为可以调度的对象。Windows选择可调度的线程内核对象中的一个,将它加载到C P U的寄存器中,它的值是上次保存在线程的环境中的值。这项操作称为上下文转换。Windows实际上保存了一个记录,它说明每个线程获得了多少个运行机会。
Windows被称为抢占式多线程操作系统,因为一个线程可以随时停止运行,随后另一个线程可进行调度。如你所见,可以对它进行一定程度的控制,但是不能太多。注意,无法保证线程总是能够运行,也不能保证线程能够得到整个进程,无法保证其他线程不被允许运行等等。
我在编写串口通讯程序的时候,起初,我有一个天真的想法,“在win32平台下,如何能够保证从串口传送过来的数据,在数据到达后1MS内开始运行?”。为此,我曾经做了许多试验,但当我真正了解了一些win32平台的知识,我得到了答案,办不到。只有实时操作系统才能作出这样的承诺,但Windows不是实时操作系统。实时操作系统必须清楚地知道它是在什么硬件上运行,这样它才能知道它的硬盘控制器和键盘等的等待时间。Microsoft对Windows规定的目标是,使它能够在各种不同的硬件上运行,即能够在不同的CPU、不同的驱动器和不同的网络上运行。简而言之,Windows没有设计成为一种实时操作系统。
Windows系统只调度可以调度的线程。那么什么是可以调度的线程,什么是不可以调度的线程呢?例如,有些线程对象的暂停计数大于1(记录在线程内核对象的上下文结构中)。这意味着该线程已经暂停运行,不应该给它安排任何C P U时间。还记得上文中曾经提到的CREATE_SUSPENDED标志吗?在创建一个线程的时候,createThread函数接收的倒数第二个参数中赋值CREATE_SUSPENDED就可以创建一个暂停的线程。除了暂停的线程外,其他许多线程也是不可调度的线程,因为它们正在等待某些事情的发生。例如,如果记事本程序,如果你不键入任何数据,那么它的线程就没有什么事情要做。系统不给无事可做的线程分配CPU时间。当移动它的窗口时,或者它的窗口需要刷新它的内容,或者将数据键入记事本,系统就会自动使它的线程成为可调度的线程。但切记,这并不意味着它的线程立即获得了CPU时间。它只是表示记事本的的线程有事情可做,系统将设法在某个时间(不久的将来)对它进行调度。
线程的暂停和执行
我们前面说过,在线程内核对象的内部有一个值,用于指明线程的暂停计数。当调用CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。这可以防止线程被调度到CPU中。当然,这是很有用的,因为线程的初始化需要时间,你不希望在系统做好充分的准备之前就开始执行线程。当线程完全初始化好了之后, 要查看是否已经传递了CREATE_SUSPENDED标志。如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么该函数将线程的暂停计数递减为0。当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。
在暂停状态中创建一个线程,就能够在线程有机会执行任何代码之前改变线程的运行环境(如优先级)。一旦改变了线程的环境,必须使线程成为可调度线程。要进行这项操作,可以调用ResumeThread,将线程句柄传递给它,如果ResumeThread,函数运行成功,它将返回线程的前一个暂停计数,否则返回0xFFFFFFFF。注意这里,它返回的是前一个暂停计数。
单个线程可以暂停若干次。如果一个线程暂停了3次,它必须恢复3次,然后它才可以被分配给一个C P U。当创建线程时,除了使用CREATE_SUSPENDED外,也可以调用SuspendThread函数来暂停线程的运行。任何线程都可以调用该函数来暂停另一个线程的运行(只要拥有线程的句柄)。不用说,线程可以自行暂停运行,但是不能自行恢复运行。SuspendThread返回的是线程的前一个暂停计数。线程暂停的最多次数可以是MAXIMUM_SUSPEND_COUNT次。值得注意的是,SuspendThread与内核方式的执行是异步进行的,但是在线程恢复运行之前,不会发生用户方式的执行。在实际环境中,调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。如果线程试图从堆栈中分配内存,那么该线程将在该堆栈上设置一个锁。当其他线程试图访问该堆栈时,这些线程的访问就被停止,直到第一个线程恢复运行。只有确切知道目标线程是什么(或者目标线程正在做什么),并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的。
线程的睡眠
线程也能告诉系统,它不想在某个时间段内被调度。这是通过调用Sleep函数来实现的:
VOID Sleep(DWORD cMilliseconds)
该函数可使线程暂停自己的运行,直到cMilliseconds过去为止。关于Sleep函数,有下面几个重要问题值得注意:
? 调用Sleep,可使线程自愿放弃它剩余的时间片。
? 系统将在大约的指定毫秒数内使线程不可调度。不错,如果告诉系统,想睡眠100ms,那么可以睡眠大约这么长时间,但是也可能睡眠数秒钟或者数分钟。还是那个反复重申的概念, Windows不是个实时操作系统。虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统
中还有什么操作正在进行。
? 可以调用Sleep,并且为cMilliseconds)参数传递INFINITE。这将告诉系统永远不要调度该线程。这不是一件值得去做的事情。最好是让线程退出,并还原它的堆栈和内核对象。
? 可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。Sleep(0)是一个非常有意思的方法。要小心Sleep()神秘的时间调整问题。Sleep()可能会使你的机器出现特别的问题。这种问题在另一台机器上可能无法再现。
切换到另一个线程
系统提供了一个称为SwitchToThread的函数,使得另一个可调度线程(如果存在能够运行)。当调用这个函数的时候,系统要查看是否存在一个迫切需要C P U时间的线程。如果没有线程迫切需要C P U时间SwitchToThread就会立即返回。如果存在一个迫切需要C P U时间的线程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要C P U时间的线程可以运行一个时间段,然后系统调度程序照常运行。该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。调用SwitchToThread函数与调用Sleep是相似的,差别是SwitchToThread允许优先级较低的线程运行。即使低优先级线程迫切需要CPU时间,而Sleep则可能因为优先级关系使得刚放弃CPU的线程被立即重新调度。
优先级
操作系统会负责为每个线程分配CPU时间。一个线程所分配到的CPU时间主要取决于该线程的优先级,而线程的优先级又取决于进程的优先级类和线程本身的相对优先级。
1. 进程的优先级类
进程的优先级类用来描述一个进程的优先程度。Win32支持四种不同的优先级类: Idle、Normal、High 和Realtime。其中,Normal是默认的优先级。在Windows单元中,每一种优先级类都对应着一个标志。当要进行进程的优先级设置时,可以用一种优先级类与CreateProcess()的参数dwCreationFlags进行或操作。另外,还可以动态地为一个已有的进程调整优先级类。这时候,通常你要用到下面API函数
bool SetPriorityClass(HANDLE hProcess,DWORD fdwPriority),其中第一个参数是进程的句柄,你可以通过GetCurrentProcess来获得当前进程的句柄。每个优先级类也对应一个数字,值在4~ 24之间。注意在Windows NT/2000下,要有特殊的权限才能修改进程的优先类。默认的设置允许进程设置它们的优先级类,但是,这些都可以由系统管理员来关闭,尤其是在高负载的WinNT/2000服务器上。
大多数情况下,进程的优先级类不要被设为Realtime。因为,大多数操作系统本身的线程的优先级类比Realtime低。如果一个进程得到的C P U时间比操作系统本身还多,后果是无法想象的。即使将进程的优先级类设为High ,也可能引起问题。因为,当高优先级的线程没有大部分空时间或等待外部事件时,它要从低优先级的线程和进程中抢夺CPU时间,直到它被一事件阻塞或处于空闲状态或处理消息。所以,在抢占式多任务操作系统中如果不能合理地安排优先级,就很容易崩溃。
优先级类 说明
实时 进程中的线程必须立即对事件作出响应,以便执行关键时间的任务。
该进程中的线程还会抢先于操作系统组件之前运行。使用本优先级类
时必须极端小心
高 进程中的线程必须立即对事件作出响应,以便执行关键时间的任务。
Task Manager(任务管理器)在这个类上运行,以便用户可以撤消脱
离控制的进程
高于正常 进程中的线程在正常优先级与高优先级之间运行(这是Wi n d o w s
2 0 0 0中的新优先级类)
正常 进程中的线程没有特殊的调度需求
低于正常 进程中的线程在正常优先级与空闲优先级之间运行(这是Wi n d o w s
2 0 0 0中的新优先级类)
空闲 进程中的线程在系统空闲时运行。该进程通常由屏幕保护程序或后
台实用程序和搜集统计数据的软件使用
2. 相对优先级
决定一个线程全面的优先级的另一方面是相对优先级。优先级类是针对进程的,它对进程内部的
所有线程都有效。而相对优先级是针对某个线程的。一个线程的相对优先级可设为以下七种: Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和Time Critical。
要设置一个线程的相对优先级,可以通过API函数SetThreadPriority来完成,再DELPHI中,你可以通过TThread对象的Priority属性来设置。获得线程相对优先级的API函数是int GetThreadPriority(HANDLE hThread);
系统何如根据优先级来调度线程
每个线程都会被赋予一个从0(最低)到31(最高)的优先级号码。当系统确定将哪个线程分配给CPU时,它首先观察优先级为31的线程,并以循环方式对它们进行调度。如果优先级为31的线程可以调度,那么就将该线程赋予一个CPU。在该线程的时间片结束时,系统要查看是否还有另一个优先级为31的线程可以运行,如果有,它将允许该线程被赋予一个CPU。只要优先级为31的线程是可调度的,系统就绝对不会将优先级为0到30的线程分配给C P U。这种情况称为渴求调度(starvation)。当高优先级线程使用大量的CPU时间,从而使得低优先级线程无法运行时,便会出现渴求情况。在多处理器计算机上出现渴求情况的可能性要少得多,因为在这样的计算机上,优先级为31和优先级为30的线程能够同时运行。系统总是设法使CPU保持繁忙状态,只有当没有线程可以调度的时候, CPU才处于空闲状态。
人们可能认为,在这样的系统中,低优先级线程永远得不到机会运行。不过正像前面指出的那样,在任何一个时段内,系统中的大多数线程是不能调度的。例如,如果进程的主线程调用GetMessage函数,而系统发现没有线程可以供它使用,那么系统就暂停进程的线程运行,释放该线程的剩余时间片,并且立即将CPU分配给另一个等待运行的线程。如果没有为GetMessage函数显示可供检索的消息,那么进程的线程将保持暂停状态,并且决不会被分配给CPU。但是,当消息被置于线程的队列中时,系统就知道该线程不应该再处于暂停状态。此时,如果没有更高优先级的线程需要运行,系统就将该线程分配给一个CPU。
高优先级线程将抢在低优先级线程之前运行,不管低优先级线程正在运行什么。例如,如果一个优先级为5的线程正在运行,系统发现一个高优先级的线程准备要运行,那么系统就会立即暂停低优先级线程的运行(即使它处于它的时间片中),并且将C P U分配给高优先级线程,使它获得一个完整的时间片。还有,当系统引导时,它会创建一个特殊的线程,称为0页线程。该线程被赋予优先级0,它是整个系统中唯一的一个在优先级0上运行的线程。当系统中没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲R A M页面置0。
动态提高线程的优先级等级
通过将线程的相对优先级与线程的进程优先级类综合起来考虑,系统就可以确定线程的优先级等级。有时这称为线程的基本优先级等级。
系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。
例如,在高优先级类进程中的一个正常优先级等级的线程的基本优先级等级是13。如果用户按下一个操作键,系统就会将一个WM_KEYDOWN消息放入线程的队列中。由于一个消息已经出现在线程的队列中,因此该线程就是可调度的线程。此外,键盘设备驱动程序也能够告诉系统暂时提高线程的优先级等级。该线程的优先级等级可能提高2级,其当前优先级等级改为15。系统在优先级为15时为一个时间片对该线程进行调度。一旦该时间片结束,系统便将线程的优先级递减1,使下一个时间片的线程优先级降为14。该线程的第三个时间片按优先级等级13来执行。如果线程要求执行更多的时间片,均按它的基本优先级等级13来执行。注意,线程的当前优先级等级决不会低于线程的基本优先级等级。此外,导致线程成为可调度线程的设备驱动程序可以决定优先级等级提高的数量。Microsoft并没有规定各个设备驱动程序可以给线程的优先级提高多少个等级。这样就使得Microsoft可以不断地调整线程优先级提高的动态等级,以确定最佳的总体响应性能。系统只能为基本优先级等级在1至15之间的线程提高其优先级等级。实际上这是因为这个范围称为动态优先级范围。此外,系统决不会将线程的优先级等级提高到实时范围(高于15)。由于实时范围中的线程能够执行大多数操作系统的函数,因此给等级的提高规定一个范围,就可以防止应用程序干扰操作系统的运行。另外,系统决不会动态提高实时范围内的线程优先级等级。
另一种情况也会导致系统动态地提高线程的优先级等级。比如有一个优先级为4的线程准备运行但是却不能运行,因为一个优先级为8的线程正连续被调度。在这种情况下,优先级为4的线程就非常渴望得到CPU时间。当系统发现一个线程在大约3至4s内一直渴望得到C P U时间,它就将这个渴望得到CPU时间的线程的优先级动态提高到15,并让该线程运行两倍于它的时间量。当到了两倍时间量的时候,该线程的优先级立即返回到它的基本优先级。
系统动态的改变优先级,在我们编程的时候会产生不良影响,为此,还有两个API函数可以使得系统的此功能不起作用。
bool SetProcessPriorityBoost(HANDLE hProcess,bool disablePriorityboost);
bool SetThreadPriorityBoost(HANDLE hThread,bool disablePriorityboost);
从名字你就应该可以看出,第一个API函数可以激活或停用指定进程所有线程的优先级提高功能,而后面一个则是针对特定线程的。
例子:关键的代码如下
...{
作者:wudi_1982
联系方式:wudi_1982@hotmail.com
转载请著名出处
本代码旨在演示线程的调度,很多位置没有加入适当的控制和资源释放,请按照后续操作执行
}
type
TSleepType=(stSleep,stSwitch);
//演示线程调度的TThread派生类
TPriThread1=class(TThread)
private
CurCount : integer; //当前计数
Flb : TLabel; //用来显示当前计数的label
FCanSleep : Boolean; //是否自动释放时间片
FSleepMs : integer;
FSleepType : TSleepType;//释放时间片的方式
procedure GetRestult;
protected
procedure Execute;override;
public
constructor Create(CreateSuspended: Boolean;ALabel : TLabel);
property CanSleep : boolean read FCanSleep write FCanSleep;
property SleepMs : integer read FSleepMs write FSleepMs;
property SleepType : TSleepType read FSleepType write FSleepType;
end;
....
...{ TPriThread1的实现 }
constructor TPriThread1.Create(CreateSuspended: Boolean; ALabel: TLabel);
begin
//构造函数
flb := ALabel;
FSleepMs := 0;
FCanSleep := true;
FSleepType := stSleep;
inherited create(CreateSuspended);
end;
procedure TPriThread1.Execute;
var
i : integer;
begin
inherited;
FreeOnTerminate := true;
CurCount := 0;
for i := 0 to 100000 do
begin
CurCount := i;//改变当前计数
Synchronize(GetRestult);//显示结果
if FCanSleep then//是否自动释放时间片
begin
//根据释放时间片的不同方式进行相应操作
case FSleepType of
stSleep : Sleep(SleepMs);//睡眠
stSwitch : SwitchToThread;//调用其他线程
end;
end;
end;
end;
procedure TPriThread1.GetRestult;
begin
flb.Caption := IntToStr(CurCount);
end;
...{Form1的主要代码}
procedure TForm1.btnPThread1CreateClick(Sender: TObject);
begin
//生成两个线程
MyPThread1 := TPriThread1.Create( not ckbx1State.Checked,lab1);
MyPThread2 := TPriThread1.Create(not ckbx2State.Checked,lab2);
//得到他们当前的优先级
lb1p.Caption := inttostr(GetThreadPriority(MyPThread1.Handle));
lb2p.Caption := inttostr(GetThreadPriority(MyPThread2.Handle));
end;
procedure TForm1.btnPThread1ResClick(Sender: TObject);
begin
//执行线程
MyPThread1.Resume;
ckbx1State.Checked := true;
MyPThread2.Resume;
ckbx2State.Checked := true;
end;
procedure TForm1.btnPThread1SudClick(Sender: TObject);
begin
//挂起线程
MyPThread1.Suspend;
ckbx1State.Checked := false;
MyPThread2.Suspend;
ckbx2State.Checked := false;
end;
procedure TForm1.btnUpPThread1Click(Sender: TObject);
begin
//在线程挂起时,提高第一个线程的相对优先级
MyPThread1.Priority := tpHigher;
//显示当前的优先级到屏幕
lb1p.Caption := inttostr(GetThreadPriority(MyPThread1.Handle));
// MyPThread2.Priority := tpHigher;
end;
procedure TForm1.btnUpdateSleepClick(Sender: TObject);
begin
//修改两个线程的时间片释放方式
MyPThread1.CanSleep := ckbxAllowSleep1.Checked;
case RadioGroup1.ItemIndex of
0 : MyPThread1.SleepType := stSleep;
1 : MyPThread1.SleepType := stSwitch;
end;
MyPThread2.CanSleep := ckbxAllowSleep2.Checked;
case RadioGroup2.ItemIndex of
0 : MyPThread2.SleepType := stSleep;
1 : MyPThread2.SleepType := stSwitch;
end;
end;
窗体效果:
让我们来用这个程序测试一些效果:
1、基本执行。程序运行之后,使用默认设置,点击【创建线程】按钮,线程将被创建,并且挂起,这是你可以间隔的点击【执行线程】和【挂起线程】按钮,你会在屏幕上看到线程的当前计数,注意这两个计数之间的差值,以及整个界面的执行效果(我指的是在你让线程不断的执行和挂起之间界面是否会出现不刷新的情况),当线程执行完毕之后,关闭程序。
2、通过Sleep(0)释放时间片演示线程调度。运行程序,使用默认设置,点击【创建线程】按钮,然后将两个线程的自释放时间片功能统统去掉(也就是去掉ckbxAllowSleep1 and 2的勾勾),然后点击【修改睡眠方式】按钮,随后你可以进行间隔点击【执行线程】和【挂起线程】按钮,多做几次这样的操作,观察两个计数之间的差值,和测试1的差值比较一下。我想你应该能想到些什么。然后,几乎可以肯定你的界面将会出现无法刷新的情况,并且你的鼠标无法立即在此界面上进行其他的操作。这个时候,稍等一下,你会发现过了一会儿,两个当前计数都被刷新了。为什么??这时,我们除了考虑我们创建的两个线程之外,你还要考虑的你程序本身的主线程以及其他可能存在的附属线程,我们再去程序中线程的那段循环代码,
CurCount := i;//改变当前计数
Synchronize(GetRestult);//显示结果
if FCanSleep then//是否自动释放时间片
begin
//根据释放时间片的不同方式进行相应操作
case FSleepType of
stSleep : Sleep(SleepMs);//睡眠
stSwitch : SwitchToThread;//调用其他线程
end;
end;
你应该看到线程将当前计数显示在屏幕上的操作是执行了Synchronize(GetRestult),这里,因为我们的线程和VCL界面发生了交互,我们必须对Synchronize有所了解,去看VCL的源码,你会发现,当你在程序中第一次创建一个附属线程时, VCL将会从主线程环境中创建和维护一个隐含的线程窗口。此窗口唯一的目的是把通过Synchronize()调用的方法排队。Synchronize()把由Method参数传递过来的方法保存在TThread的FMethod字段中,然后,给线程窗口发一个CM_EXECPROC消息,并且把消息的lParam参数设为self(这里指线程对象)。当线程窗口的窗口过程收到这个消息后,它就调用FMethod字段所指定的方法。由于线程窗口是在主线程内创建的,线程窗口的窗口过程也将被主线程执行。因此,FMethod字段所指定的方法就在主线程内执行。
在我们选择释放时间片的模式下,在这里,无论我们是用Sleep还是SwitchToThread,当前线程都会立即释放时间片,因为这时我们并没有修改线程的优先级,他们都在同样的优先级环境下运行,那么当占用CPU的线程释放时间片后,其他线程将可以相对轻松的得到CPU,所以在使用释放时间片的模式下,界面的刷新会良好。并且调度相对有序。
3、Sleep和SwitchToThread区别的演示。运行程序,使用默认设置,点击【创建线程】按钮,然后点击【提高线程1的优先级】按钮,再点击【执行线程】这是,两个线程将不再是同样的优先级,其他设置依然是默认的(使用sleep方式释放时间片),你会看到线程1首先执行,线程2处于可调度模式,但并没有被调度(当前计数没有刷新),并且屏幕也不刷新,在稍等一段时间之后,屏幕刷新,线程2也开始运行,并且此时屏幕刷新正常。为什么呢?回头去看本文上面的内容,当线程1的优先级提高之后,系统会首先调度它,虽然它使用sleep(0)来释放时间片,但当时间片释放后,因为它的优先级相对较高,系统依然会调度线程1,所以此时,线程2将不能执行,界面也不能有效刷新。在这个思路下,再做一个测试,使用同样的方式,只不过这次,在线程执行之前,除了提高线程1的优先级之外,还将线程1释放时间片方式改为SwitchToThread,此时你就可以看到两个线程都有机会执行,并且界面也将有效刷新。
4、你还可以做其他配置信息的测试,相信会加深对WIN32平台下线程调度的了解。
参考文献
1、《DELPHI5开发人员指南》
2、《WINDOWS核心编程》
注:以上程序在D7+WINXP下测试通过
线程中的变量
由于每个线程都代表了一个不同的执行路径,因此,最好有一种只限于一个线程内部使用的数据,
要实现上述目的有以下几种方式:
1、局部变量(基于栈),很简单,在你的线程函数中你定义的变量既是如此。由于每个线程都在各自的栈中,各个线程将都有一套局部变量的副本,这样,就不会相互影响。对于那些只在过程或函数的生存期有意义的变量,应当把它们声明为局部变量。
2、存储在线程对象中。还记得CreateThread函数中的lpParameter参数吗,它可以接受一个无类型的指针。结合本文第一章的内容,你应该还记得,它被存储在线程内核对象的上下文结构中,你可以通过CONTEXT结构中的CONTEXT_INTEGER部分的ebx来读取它的地址。
下面是一段示例代码,用来演示读取CONTEXT结构,这段代码一般用不到,但它可以说明CrateThread函数中的lpParameter被存储的位置
type
//传递给线程函数的结构和指针的声明
Tinfo = record
count : integer;
x : integer;
y : integer;
end;
Pinfo= ^Tinfo;
var
MyThreadHad : THandle;//一个全局变量,用来保存线程的句柄
//线程函数
function MyThread(info : Pointer):DWORD; stdcall;
var
i : integer;
begin
//根据传递来信息决定在窗口的那个位置输出什么信息
for i := 0 to Pinfo(info)^.count-1 do
Form1.Image1.Canvas.TextOut(Pinfo(info)^.x,Pinfo(info)^.y,inttostr(i));
//FreeMem(info);
Result := 0;
end;
//创建一个线程
procedure TForm1.Button4Click(Sender: TObject);
var
ppi : Pinfo;
MyThreadId : DWORD;
begin
//分配空间并赋初值
ppi :=AllocMem(sizeof(tinfo));
ppi^.count := 1000000;
ppi^.x := 10;
ppi^.y := 10;
//创建
MyThreadHad := CreateThread(nil,0,@MyThread,ppi,CREATE_SUSPENDED,MyThreadId);
//在窗体上显示线程函数的地址和传递给它的参数的地址
labThreadAddr.Caption := inttostr( integer(@MyThread));
labThreadPvparam.Caption := inttostr(integer(ppi));
end;
//读取CONTEXT结构,注意CONTEXT结构是和CPU有关的,我这里测试时,工作在intel的CPU上
procedure TForm1.btnRContextClick(Sender: TObject);
var
con : _CONTEXT;
begin
//初始化结构
con.ContextFlags := CONTEXT_FULL;
//读取
GetThreadContext(MyThreadHad,con);
//显示在窗体的listbox上
with lbxContextInfo.Items do
begin
// Clear;
Add('------------CONTEXT--------------');
Add('');
Add('CONTEXT_DEBUG_REGISTERS-----');
Add('dr0:'+#9+IntToStr(con.Dr0));
Add('dr1:'+#9+IntToStr(con.Dr1));
Add('dr2:'+#9+IntToStr(con.Dr2));
Add('dr3:'+#9+IntToStr(con.Dr3));
Add('dr6:'+#9+IntToStr(con.Dr6));
Add('dr7:'+#9+IntToStr(con.Dr7));
add('CONTEXT_SEGMENTS---------');
Add('segGs:'+#9+inttostr(con.SegGs));
Add('segFs:'+#9+inttostr(con.Segfs));
Add('segEs:'+#9+inttostr(con.Seges));
Add('segDs:'+#9+inttostr(con.Segds));
Add('CONTEXT_INTEGER.---------');
Add('edi: '+#9+IntToStr(con.Edi));
Add('esi: '+#9+IntToStr(con.Esi));
Add('ebx: '+#9+IntToStr(con.Ebx));
Add('edx: '+#9+IntToStr(con.Edx));
Add('ecx: '+#9+IntToStr(con.Ecx));
Add('eax: '+#9+IntToStr(con.Eax));
Add('CONTEXT_CONTROL----------');
Add('Ebp: '+#9+IntToStr(con.Ebp));
Add('Eip: '+#9+IntToStr(con.Eip));
Add('segcs: '+#9+IntToStr(con.SegCs));
Add('EFlags: '+#9+IntToStr(con.EFlags));
Add('Esp: '+#9+IntToStr(con.Esp));
Add('SegSs: '+#9+IntToStr(con.SegSs));
end;
end;
把上面代码整理之后,添加到你的程序中,你可以发现(如果也是intel的CPU),那么你可以从Eax寄存器读取到线程函数的地址,从Ebx中读取到传递给线程函数的参数地址。
在DELPHI中的TThread对象的构造函数中,你可以看到这段代码
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
再观察BeginThread的实现,你会发现TThread的调用CreateThread时,将Pointer(Self),也就是TThread对象本身当作线程函数的参数传递过去,换言之,你在TThread的派生类中定义的变量,对于一个线程而言,将存储在这个线程单独的堆栈中,而它在堆栈的地址存储在线程的上下文结构中。
可以做一个简单的试验,将一个线程生成多次,你可以发现存储在线程对象内部的变量将互不影响。
说到这里,必须谈论一个问题,效率的问题,我在一本书上曾经看到过这样一段话“由于访问线程对象中的数据比访问线程局部变量要快10倍,因此,你应当尽可能地把线程专用的信息保存在线程对象中。”对此,我一直没有特别理解。如果一定要相信这句话,那我会这么理解,就是存储在线程对象中的变量因为上下文结构记录了它的地址等原因,所以它更快。尽信书不如无书,我还在思考,不过好在这种速度的影响对于通常的使用而言影响不大。
3、在DELPHI中,用Object Pascal的关键字threadvar来声明变量,以利用操作系统级的线程局部存储。
在前面我们了解到:虽然对于局部变量,在每个线程中都一个副本,然而应用程序的全局变量是被所有线程所共享的。当多个线程对这个全局变量进行访问时,将可能出现很多未知的问题,Win32提供了一种称为线程局部存储的方式,它能使你在第一个运行的线程中创建一个全局变量的拷贝。Delphi利用关键字threadvar封装此功能。在threadvar关键字下你可以声明任何局部存储的变量。
4、全局变量,多线程最让人头疼的地方就是全局变量了,好的同步方式将决定你高效、安全的访问全局变量,虽然上述的threadvar是解决全局变量线程局部存储的一个办法,但在我实际的编码工作中,几乎很少用它,它的局限性太多。多线程访问全局变量的方法将在下一文中详细描述。